JavaScript:Promises 以及 Async/Await 为何胜出 Promises vs. Callbacks 🥊 Promises 🤞 Async/Await? 🤔 最后的想法 📃

2025-05-25

JavaScript:Promises 以及 Async/Await 为何胜出

Promises 与 Callbacks 🥊

承诺🤞

异步/等待?🤔

最后的想法📃

异步/等待

在 JavaScript 中,异步函数有好有坏。优点是异步函数是非阻塞的,因此速度很快——尤其是在 Node.js 环境中。缺点是处理异步函数可能很麻烦,因为有时你必须等待一个函数完成才能获得它的“回调”,然后才能继续执行下一个函数。

有很多方法可以充分发挥异步函数调用的优势并妥善处理它们的执行,但其中有一种方法远胜于其他所有方法(剧透:那就是 Async/Await)。在这篇快速阅读中,你将了解Promises的来龙去脉以及Async/Await的使用,以及我们对两者进行比较的看法。

享受

Promises 与 Callbacks 🥊

作为 JavaScript 或 Node.js 开发人员,正确理解 Promises 和 Callbacks 之间的区别以及它们如何协同工作至关重要。

两者之间存在细微但重要的区别。每个 Promise 的核心都有一个回调函数,用于解析某种数据(或错误),这些数据(或错误)会冒泡到被调用的 Promise 中。

回调处理程序:

function done(err) {
    if (err) {
        console.log(err);
        return;
    }

    console.log('Passwords match!');
}
Enter fullscreen mode Exit fullscreen mode

调用validatePassword()函数:

function validatePassword(password) {
    if (password !== 'bambi') {
        return done('Password mismatch!');
    }

    return done(null);
}
Enter fullscreen mode Exit fullscreen mode

下面的代码片段展示了验证密码的完整端到端检查(它是静态的,并且必须与我小时候最喜欢的卡通人物“bambi”匹配)

// provided a string (password)
function validatePassword(password) {
    // create promise with resolve and reject as params
    return new Promise((resolve, reject) => {
        // validate that password matches bambi (the deer)
        if (password !== 'bambi') {
            // password doesn't match, return an error with reject
            return reject('Invalid Password!');
        }

        // password matches, return a success state with resolve
        resolve();
    });
}

function done(err) {
    // if an err was passed, console out a message
    if (err) {
        console.log(err);
        return; // stop execution
    }

    // console out a valid state
    console.log('Password is valid!');
}

// dummy password
const password = 'foo';

// using a promise, call the validate password function
validatePassword(password)
    .then(() => {
        // it was successful
        done(null);
    })
    .catch(err => {
        // an error occurred, call the done function and pass the err message
        done(err);
    });
Enter fullscreen mode Exit fullscreen mode

代码注释得很好,但是,如果你感到困惑,catch 仅在reject()Promise 中调用 a 时才会执行。由于密码不匹配,我们调用reject(),从而“捕获”错误并将其发送给done()函数。

承诺🤞

与传统的基于回调的方法相比,Promise 提供了一种更简单的执行、组合和管理异步操作的替代方案。它还允许您使用类似于同步 try/catch 的方法来处理异步错误。

Promises 还提供了三种独特的状态

  1. 待定- 承诺的结果尚未确定,因为产生其结果的异步操作尚未完成。
  2. 已实现- 异步操作已完成,并且承诺具有价值。
  3. 已拒绝- 异步操作失败,Promise 永远不会被履行。在已拒绝状态下,Promise 会给出一个原因,表明操作失败的原因。

当 Promise 处于待处理状态时,它可以转换为已完成或已拒绝状态。然而,一旦 Promise 已完成或已拒绝,它就永远不会转换为任何其他状态,其值或失败原因也不会改变。

缺点👎

Promise 无法解决所谓的“回调地狱”,它实际上只是一系列嵌套的函数调用。当然,对于一次调用来说还好。但如果调用次数过多,你的代码就会变得难以阅读和维护,甚至无法阅读和维护。

Promises 循环

为了避免 JavaScript 中深度嵌套的回调,人们会认为可以简单地循环遍历 Promise,将结果返回给对象或数组,并在完成后停止。不幸的是,这并不容易;由于 JavaScript 的异步特性,如果循环遍历每个 Promise,则不会在代码完成时调用“done”事件。

处理这种情况的正确方法是使用Promise.all()。此函数会等待所有完成(或第一次拒绝)后才将其标记为完成。

错误处理💣

使用多个嵌套的 Promise 调用进行错误处理就像蒙着眼睛开车一样。你只能靠运气才能找到哪个 Promise 抛出了错误。最好的办法是catch()完全删除该方法,并选择使用全局错误处理程序(然后祈祷好运),如下所示:

浏览器:

window.addEventListener('unhandledrejection', event => {
    // can prevent error output on the console:
    event.preventDefault();

    // send error to log server
    log('Reason: ' + event.reason);
});
Enter fullscreen mode Exit fullscreen mode

Node.js:

process.on('unhandledRejection', (reason) => {
    console.log('Reason: ' + reason);
});
Enter fullscreen mode Exit fullscreen mode

注意:以上两种方法只能确保捕获错误。如果您漏掉了添加catch()方法,它将被代码覆盖。

异步/等待?🤔

Async/Await 允许我们编写看起来像同步的异步 JavaScript。在本文的前几部分,我们介绍了 Promises——它本简化异步流程并避免回调地狱——但事实并非如此。

回调地狱?🔥

回调地狱是用来描述以下场景的一个术语:

注意:例如,这里有一个 API 调用,它将从数组中获取 4 个特定用户。

// users to retrieve
const users = [
    'W8lbAokuirfdlTJpnsNC5kryuHtu1G53',
    'ZinqxnohbXMQdtF6avtlUkxLLknRxCTh',
    'ynQePb3RB2JSx4iziGYMM5eXgkwnufS5',
    'EtT2haq2sNoWnNjmeyZnfUmZn9Ihfi8w'
];

// array to hold response
let response = [];

// fetch all 4 users and return responses to the response array
function getUsers(userId) {
    axios
        .get(`/users/userId=${users[0]}`)
        .then(res => {
            // save the response for user 1
            response.push(res);

            axios
                .get(`/users/userId=${users[1]}`)
                .then(res => {
                    // save the response for user 2
                    response.push(res);

                    axios
                        .get(`/users/userId=${users[2]}`)
                        .then(res => {
                            // save the response for user 3
                            response.push(2);

                            axios
                                .get(`/users/userId=${users[3]}`)
                                .then(res => {
                                    // save the response for user 4
                                    response.push(res);
                                })
                                .catch(err => {
                                    // handle error
                                    console.log(err);
                                });
                        })
                        .catch(err => {
                            // handle error
                            console.log(err);
                        });
                })
                .catch(err => {
                    // handle error
                    console.log(err);
                });
        })
        .catch(err => {
            // handle error
            console.log(err);
        });
}
Enter fullscreen mode Exit fullscreen mode

哎呀,这代码太丑了,还占用了一大堆空间。Async/Await 是 JavaScript 最新最强大的功能,它不仅能让我们避免回调地狱,还能确保代码简洁,错误也能被正确捕获。我发现 Async/Await 最吸引人的地方在于,它建立在 Promises(非阻塞等)之上,却又保持了代码的可读性,读起来就像同步代码一样。这就是它的强大之处。

注意:下面是同一组 API 调用的示例,用于从数组中检索 4 个用户,代码行数超过一半:

// users to retrieve
const users = [
    'W8lbAokuirfdlTJpnsNC5kryuHtu1G53',
    'ZinqxnohbXMQdtF6avtlUkxLLknRxCTh',
    'ynQePb3RB2JSx4iziGYMM5eXgkwnufS5',
    'EtT2haq2sNoWnNjmeyZnfUmZn9Ihfi8w'
];

// array to hold response
let response = [];

async function getUsers(users) {
    try {
        response[0] = await axios.get(`/users/userId=${users[0]}`);
        response[1] = await axios.get(`/users/userId=${users[1]}`);
        response[2] = await axios.get(`/users/userId=${users[2]}`);
        response[3] = await axios.get(`/users/userId=${users[3]}`);
    } catch (err) {
        console.log(err);
    }
}
Enter fullscreen mode Exit fullscreen mode

很奇特吧?💃

而且由于 Async/Await 建立在 Promises 之上,您甚至可以使用Promise.all()await 关键字:

async function fetchUsers() {
  const user1 = getUser1();
  const user2 = getUser2();
  const user3 = getUser3();

  const results = await Promise.all([user1, user2, user3]);
}
Enter fullscreen mode Exit fullscreen mode

注意:由于 Async/await 的同步特性,其速度会略慢。连续多次使用它时应格外小心,因为 await 关键字会停止其后所有代码的执行——就像在同步代码中一样。

如何开始使用 Async/Await?💻

Async/Await 的理解和使用出奇地简单。事实上,它已经内置Node.js 的最新版本中,并且正在快速向浏览器端迁移。目前,如果你想在客户端使用它,你需要使用Babel,这是一个易于使用和设置的 Web 转译器。

异步

让我们从 async 关键字开始。它可以放在函数之前,如下所示:

async function returnTrue() {
  return true;
}
Enter fullscreen mode Exit fullscreen mode

等待

关键字 await 使 JavaScript 等待该 Promise 完成并返回其结果。以下是示例:

let value = await promise; // only works inside of an async function
Enter fullscreen mode Exit fullscreen mode

完整示例

// this function will return true after 1 second (see the async keyword in front of function)
async function returnTrue() {

  // create a new promise inside of the async function
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve(true), 1000) // resolve
  });

  // wait for the promise to resolve
  let result = await promise;

  // console log the result (true)
  console.log(result);
}

// call the function
returnTrue();
Enter fullscreen mode Exit fullscreen mode

为什么 Async/Await 更好?😁

现在我们已经了解了 Promises 和 Async/Await 提供的许多功能,让我们回顾一下为什么我们(Stream)认为 Async/Await 是我们代码库的最佳选择。

  1. Async/Await 可以简化代码,减少代码行数、减少输入、减少错误,从而打造简洁明了的代码库。最终,它使复杂的嵌套代码再次变得可读。
  2. 使用 try/catch 进行错误处理(在一个地方,而不是在每次调用时)
  3. 错误堆栈很有意义,这与 Promises 中那些模糊的堆栈不同,Promises 的错误堆栈很大,很难定位错误的来源。最重要的是,错误会指向产生错误的函数。

最后的想法📃

我可以说 Async/Await 是过去几年添加到 JavaScript 的最强大的功能之一。

我们花了不到一天的时间就理解了语法,并发现代码库在这方面有多混乱。我们总共花了大约两天时间将所有基于 Promise 的代码转换为 Async/Await,这实际上是一次彻底的重写——这也充分说明了使用 Async/Await 时所需的代码量有多么少。

最后,感谢您阅读这篇文章。如果您对我在 Stream 的工作感兴趣,不妨试试我们5 分钟的 API 教程——我保证绝对值得。想了解更多精彩文章,您也可以在 Twitter 上关注我:@nickparsons

祝你编程愉快!🤓

文章来源:https://dev.to/nickparsons/javascript-promises-and-why-asyncawait-wins-the-battle-1g8a
PREV
使用 Django 和 React 的 Python 聊天教程
NEXT
为 95% 而非 5% 优化编程决策