一个关于async/await有趣的解释

180 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第22天,点击查看活动详情

在js中,我们有三种编写异步代码的方式。
第一种方法是使用回调。 当异步操作完成时,会执行一个回调函数(意思是操作完成后给我回电话):

const callbackFunction = result = {

// Called when the operation completes

};

asyncOperation(params, callbackFunction);

但是一旦处理多个异步操作,回调函数就会相互嵌套,从而导致回调地狱。
承诺是一个异步操作的结果占位符。 通过使用 Promise,可以更轻松地处理异步操作。

const promise = asyncOperation(params);

promise.then(result => {

// Called when the operation completes

});

你见过.then().then()...then() 承诺链🚂🚃🚃🚃🚃吗? Promise的一个问题是它们有的时候比较冗长。

最后,第三次突破是 async/await 语法(从 ES2017 开始)。 它让我们以简洁和同步的方式编写异步代码:

(async function() {

const result = await asyncOperation(params);

// Called when the operation completes

})();

在这篇文章中,将逐步解释如何在 JavaScript 中使用 async/await。

注意:async/await 是 Promise 之上的语法糖。

1.同步添加

因为标题提到了一个有趣的解释,下面将逐步解释关于贪婪老板的async/await故事。

让我们从一个简单的(同步)函数开始,它的任务是计算加薪:

function increaseSalary(base, increase) {

    const newSalary = base + increase;

    console.log(`New salary: ${newSalary}`);

    return newSalary;

}

increaseSalary(1000, 200); // => 1200

// logs "New salary: 1200"

increaseSalary() 是一个对 2 个数字求和的函数。 n1 + n2 是同步操作。

老板不希望员工的工资快速增加☹。 所以你不能在increaseSalary() 函数中使用加法运算符+。

相反,您必须使用一个需要消耗2秒时间的慢速函数来汇总数字。 让我们将函数命名为 slowAddition():

function slowAddition(n1, n2) {

    return new Promise(resolve => {

        setTimeout(() => resolve(n1 + n2), 2000);

    });

}

slowAddition(1, 5).then(sum => console.log(sum));

// 2秒后打印 "6"

slowAddition() 返回一个 promise,它在 2 秒的后resolve为参数的和。

如何更改increaseSalary()函数来支持慢加?

2.异步添加

第一个初步的尝试是用slow Addition(n1, n2)的调用替换 n1 + n2 表达式:

function increaseSalary(base, increase) {

    const newSalary = slowAddition(base, increase);

    console.log(`New salary: ${newSalary}`);

    return newSalary;

}

increaseSalary(1000, 100); // => [object Promise]

// Logs "New salary [object Promise]"

不幸的是,函数increaseSalary() 不知道如何处理promise。 该函数把promise当成常规对象:它不知道如何以及何时从promise 中提取值。

现在是时候使用async/await语法,让increaseSalary() 知道如何处理slowAddition()返回的promise。

首先,您需要在函数声明前面添加 async 关键字。 然后,在函数体内部,需要使用await 操作符让函数等待promise 被resolve。

让我们对 increaseSalary() 函数进行这些更改:

async function increaseSalary(base, increase) {

    const newSalary = await slowAddition(base, increase);

    console.log(`New salary: ${newSalary}`);

    return newSalary;

}

increaseSalary(1000, 200); // => [object Promise]

// 2秒后打印 "New salary 1200"

JavaScript 以如下方式计算处理 const newSalary = await slowAddition(base, increase):

  1. await slowAddition(base, increase) 暂停 increaseSalary() 函数执行
  2. 2秒后slowAddition(base, increase)返回的promise resolved了
  3. increaseSalary() 函数执行恢复
  4. newSalary 被分配了promise的resolve值1200 (1000+200)
  5. 函数执行照常继续。

简单来说,当 JavaScript 在 async 函数中遇到 await promise 时,它会暂停函数执行,直到 promise 被resolve。 承诺的resolve值成为await promise的结果。

即使 return newSalary 返回数字 1200,如果您查看函数 increaseSalary(1000, 200) 返回的实际值——它仍然是一个承诺!

异步函数总是返回一个promise,它的resolve值为函数内返回值:

increaseSalary(1000, 200).then(salary => {

salary; // => 1200

});

返回 promise 的异步函数是一件好事,因为这样我们就可以嵌套异步函数。

3.打破异步加法

老板要求慢慢加薪是不公平的。你决定破坏slowAddition() 函数。

你修改慢加函数以拒绝数字加法:

function slowAdditionBroken(n1, n2) {

    return new Promise((resolve, reject) => {

        setTimeout(() => reject(new Error('Unable to sum numbers')), 3000);

    });

}
slowAdditionBroken(1, 5).catch(e => console.log(e.message));

// 3秒后打印 "Unable to sum numbers"

如何在异步函数calculateSalary()中处理被rejected的promise? 只需将 await 运算符写在 try/catch 子句中:

async function increaseSalaryBroken(base, increase) {
  let newSalary;
  try {
    newSalary = await slowAdditionBroken(base, increase);
  } catch (e) {
    console.log(`Error: ${e.message}`);
    newSalary = base * 2;
  }
  console.log(`New salary: ${newSalary}`);
  return newSalary;
}
increaseSalaryBroken(1000, 200);
// 3秒后打印
// "Error: Unable to sum numbers", "New salary: 2000"

在表达式 await slowAdditionBroken(base, increase) 处,JavaScript 暂停函数执行并等待promise fulfilled(承诺成功 resolved)或被rejected(发生错误)。

3 秒后,promise 被rejected并返回 new Error('Unable to sum numbers')。 由于rejected,函数的执行会跳转到基本工资乘以2的catch (e){ } 子句中。

吝啬鬼付了两次钱。 现在老板要付双倍工资。

如果您没有捕获被rejected的promise,则错误会传播,并且异步函数返回的承诺也是rejected状态:

async function increaseSalaryBroken(base, increase) {

    const newSalary = await slowAdditionBroken(base, increase);

    return newSalary;

}

increaseSalaryBroken(1000, 200).catch(e => {

    e.message; // => "Unable to sum numbers"

});

4.嵌套异步函数

尽管在异步函数中 return value 表达式返回有效值而不是承诺,但当异步函数被调用时,它仍然返回一个承诺。

这是一件好事,因为 我们可以借此来嵌套异步函数!

例如,让我们编写一个异步函数,使用 slowAddition() 函数增加工资数组:

async function increaseSalaries(baseSalaries, increase) {

    let newSalaries = [];

    for (let baseSalary of baseSalaries) {

        newSalaries.push(

            await increaseSalary(baseSalary, increase);

        );

    }

    console.log(`New salaries: ${newSalaries}`);

    return newSalaries;

}

increaseSalaries([950, 800, 1000], 100);

// 6秒后打印 "New salaries: 1050,900,1100"

await increaseSalary(baseSalary, increase) 被调用 3 次。 每次 JavaScript 等待 2 秒,直到计算总和。

通过这种方式,您可以将异步函数嵌套到异步函数中。

5.并行异步

在前面对工资数组求和的示例中,求和按顺序进行:函数对每个工资暂停 2 秒。
但是你可以同时加薪! 让我们使用 Promise.all() 实用函数来同时开始所有的加薪:

async function increaseSalaries(baseSalaries, increase) {

    let salariesPromises = [];

    for (let baseSalary of baseSalaries) {

        salariesPromises.push(

        increaseSalary(baseSalary, increase)

        );

    }

    const newSalaries = await Promise.all(salariesPromises);

    console.log(`New salaries: ${newSalaries}`);

    return newSalaries;

}

increaseSalaries([950, 800, 1000], 100);

// 2秒后打印 "New salaries: 1050,900,1100"

加薪任务立即开始(在increaseSalary(baseSalary, increase)附近不使用await),并在salaryPromises 中收集promise。

await Promise.all(salariesPromises) 暂停函数执行,直到所有并行处理的异步操作完成。 最后,仅在 2 秒后,newSalaries 变量包含所有增加的工资。

您已经设法在短短 2 秒内增加了所有员工的工资,即使每个操作都很慢并且需要 2 秒。 你又骗老大了!

6.实际异步示例

使用 async/await语法常见情况是fetch远程数据。 fetch() 方法非常适合与 async/await 一起使用,因为它返回一个可resolve为远程 API 返回值的promise。

例如,以下是从远程服务器获取电影列表的方法:

async function fetchMovies() {
  const response = await fetch('https://api.example.com/movies');
  if (!response.ok) {
    throw new Error('Failed to fetch movies');
  }
  const movies = await response.json();
  return movies;
}

await fetch('api.example.com/movies') 将暂停 fetchMovies() 执行,直到请求完成。 然后你可以使用await response.json()获取结果。

7.总结

async/await 是 Promise 之上的语法糖,它提供了一种以同步方式处理异步任务的方法。 async/await 有 4 个简单的规则:

  1. 处理异步任务的函数必须使用 async 关键字进行标记。
  2. await promise 运算符会暂停函数执行,直到 promise 成功resolved 或 rejected.。
  3. 如果promise 成功resolve,await 操作符返回解析值:const resolutionValue = await promise。 否则,您可以在 try/catch 中捕获被rejected的promise。
  4. 异步函数总是返回一个promise,这使异步函数有了嵌套的能力。