JavaScript 中 async/await 的有趣解释

JavaScript 中,有三种方式编写异步任务。

第一种方法是使用回调。当一个异步操作完成后,执行一个回调函数(意思是当操作完成后再回调我):

const callbackFunction = (result = {
  // Called when the operation completes
});
asyncOperation(params, callbackFunction);

但只要处理多个异步操作,回调函数就会互相嵌套,最后陷入回调地狱。

promise 是一个异步任务结果的占位对象。使用 promise,你可以更容易地处理异步操作。

const promise = asyncOperation(params);
promise.then((result) => {
  // Called when the operation completes
});

有没有看到 .then().then()...then() promise 链 🚂🚃🚃🚃🚃?promise 问题是它们很冗长。

最终,async/await 语法被设计出来(ES2017)。它可以让你简单地以同步方式编写异步代码:

(async function () {
  const result = await asyncOperation(params);
  // Called when the operation completes
})();

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

注意:async/await 是在 promise 之上的语法糖。我建议在继续之前先熟悉一下 promise (opens new window)

同步扩展

因为帖子的标题提到了一个有趣的解释,所以我将就一个贪婪的老板的故事逐步解释 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));
// After 2 seconds logs "6"

slowAddition() 返回一个承诺,在延迟 2 秒后解析为参数之和。如何更新 increaseSalary() 函数来支持慢加法?

异步扩展

第一个天真地尝试是用调用 slowAddition(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 中提取值。

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

首先,你需要在函数声明附近添加 async 关键字。然后,在函数体内部,你需要使用 await 操作符来使函数等待 promise 被解析。

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

 
 







async function increaseSalary(base, increase) {
  const newSalary = await slowAddition(base, increase);
  console.log(`New salary: ${newSalary}`);
  return newSalary;
}
increaseSalary(1000, 200); // => [object Promise]
// After 2 seconds logs "New salary 1200"

JavaScript 以如下方式评估 const newSalary = await slowAddition(base, increase)

  1. await slowAddition(base, increase) 暂停了 increaseSalary() 函数的执行。
  2. 在 2 秒后,由 slowAddition(base,increase) 返回的承诺会被解析出来。
  3. increaseSalary() 函数恢复执行。
  4. newSalary 是用 promise 的解析值 1200(1000 + 200) 来分配的。
  5. 函数执行照常进行。

简单来说,当 JavaScript 在一个异步函数中遇到 await promise 时,它会暂停函数的执行,直到 promise 被解析。promise 的解析值就成了 await promise 评估的结果。

尽管 return newSalary 返回的是数字 1200,但如果你看看函数 increaseSalary(1000, 200) 返回的实际值--它仍然是一个 promise!

一个 async 函数总是返回一个 promise,它解析为函数体内部的返回值:

increaseSalary(1000, 200).then((salary) => {
  salary; // => 1200
});

async 函数返回 promise 是一件好事,因为你可以嵌套异步函数。

错误异步扩展

老板提出了慢慢加薪的要求,这是不公平的。你决定破坏慢速加法 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));
// After 3 seconds logs "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);
// After 3 seconds logs
// "Error: Unable to sum numbers", "New salary: 2000"

在表达式 await slowAdditionBroken(base, increase) 处,JavaScript 暂停函数的执行,并等待 promise 的实现(promise 成功解决)或拒绝(发生错误)。

3 秒后,用 new Error('Unable to sum numbers') 拒绝 promise。因为拒绝,函数执行跳入 catch (e){ } 子句,在该子句中,基本工资乘以 2。

Miser 支付了两倍的工资。现在老板要支付双倍工资。很好!

如果你没有抓住一个被拒绝的 promise,那么错误就会传播,async 异步函数返回的 promise 就会被拒绝:

async function increaseSalaryBroken(base, increase) {
  const newSalary = await slowAdditionBroken(base, increase);
  return newSalary;
}
increaseSalaryBroken(1000, 200).catch((e) => {
  e.message; // => "Unable to sum numbers"
});

嵌套异步函数

尽管在异步函数内部的 return <value> 表达式返回的是有效载荷值而不是 promise,但当异步函数被调用时,它仍然会返回一个 promise。

这是件好事,因为你可以嵌套异步函数!

例如,让我们写一个使用 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);
// After 6 seconds logs "New salaries: 1050,900,1100"

await salaryIncrease(baseSalary, increase) 对数组中的每个工资都被调用 3 次。每次 JavaScript 都会等待 2 秒,直到计算出总和。

这样就可以把 async 函数嵌套到 async 函数中。

并行 async

在前面的求和工资数组的例子中,求和是按顺序进行的:每增加一个工资,函数就暂停 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);
// After 2 seconds logs "New salaries: 1050,900,1100"

加薪任务马上开始(在 increaseSalary(baseSalary, increase) 附近没有使用 await),并在 salariesPromises 中收集 promise。

await Promise.all(salitsPromises) 则暂停函数的执行,直到所有并行处理的异步操作完成。最后,只有在 2 秒后,newSalaries 变量才会包含增加的薪水。

你成功地在短短 2 秒内增加了所有员工的工资,即使每个操作都很慢,需要 2 秒。你又骗过了老板!

async 函数例子

当你想使用 async/await 语法时,常见情况是获取远程数据。

fetch() 方法是与 async/await 一起使用的一个很好的候选方法,因为它返回一个 promise,该 promise 解析到远程 API 返回的值。

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

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('https://api.example.com/movies') 要暂停执行 fetchMovies(),直到请求完成。然后你可以使用 await response.json() 来提取实际。

总结

async/await 是在 promise 之上的语法糖,提供了一种以同步方式处理异步任务的方法。

async/await 有 4 条简单的规则:

  1. 一个处理异步任务的函数必须使用 async 关键字来标记。
  2. await promise 操作符暂停函数的执行,直到 promise 被成功解析或被拒绝。
  3. 如果 promise 解析成功, await 操作符返回解析值: const resolvedValue = await promise。否则,你可以在 try/catch 里面捕获一个被拒绝的 promise。
  4. 一个异步函数总是返回一个 promise,这就提供了嵌套异步函数的能力。