原文链接:http://javascript.info/async-await,translate with ❤️ by zhangbao.

JavaScript 提供了一种更加优雅地操作 Promise 对象的语法,称为“async/await”,是相当容易理解和使用的。

异步函数

我们从 async 关键字说起。它可以以下面的方式,替换之前的函数:

  1. async function f() {
  2. return 1;
  3. }

函数之前的“async”的意思很简单:函数总是返回一个 Promise 对象。如果代码中出现 return <non-prpmise> 的地方,那么 JavaScript 会自动将这个值包装起来,返回一个 resolved 状态的 Promise 对象。

例如,上面函数返回的 Promise 对象,携带了数值 1 作为参数,我们测试一下:

  1. async function f() {
  2. return 1;
  3. }
  4. f().then(alert); // 1

……我们也可以显式的返回一个 Promise,获得一样的效果:

  1. async function f() {
  2. return Promise.resolve(1);
  3. }
  4. f().then(alert); // 1

因此,async 保证函数始终返回一个 Promise 对象;并且会把非 Promise 对象包装成 Promise 对象返回。很简单,对吧?但不只是这样。还有另外一个关键字 await——它只能在使用了 async 声明的函数中使用,非常酷。

await

语法:

  1. // 只能在使用了 async 声明的函数中使用
  2. let value = await promise;

JavaScript 碰到 await 关键字时,会等待后面的 Promise 解决(settles)并返回结果。

下面的例子 1 秒钟后随着 Promise 对象的状态变成 resolved,打印出 "done!" 这个字符串:

  1. async function f() {
  2. let promise = new Promise((resolve, reject) => {
  3. setTimeout(() => resolve("done!"), 1000)
  4. });
  5. let result = await promise; // 一直等到 promise resolved 了 (*)
  6. alert(result); // "done!"
  7. }
  8. f();

函数会在 (*) 处暂停,一直等到 Promise 解决,将结果赋值给 result。所以上面的代码在 1 秒后返回“done!”。

我们强调一下:await 关键字让 JavaScript 等待,直到 Promise 被解决,然后继续执行代码。这并不消耗任何 CPU 资源,因为引擎可以在这期间可以执行其他任务——比如执行其他脚本、处理事件等等。

这是一种比使用 promise.then 更加优雅的方式,更容易阅读和书写。

异步函数 - 图1不能再普通函数中使用 await

我们尝试在非 async 函数中使用 await 会有什么结果呢?会报错:

  1. function f() {
  2. let promise = Promise.resolve(1);
  3. let result = await promise; // Syntax error
  4. }

如果我们忘记在函数前面加 async,就会出错。因为 await 仅支持在 async 函数中使用。

我们把《Promise 链式调用》一章里的 showAvatar() 例子拿来,用 async/await 重写:

  1. 我们要把 .then 替换成 await 调用。

  2. 在函数前面加上 async,让内部支持 await 关键字的使用。

  1. async function showAvatar() {
  2. // 读取 JSON 数据
  3. let response = await fetch('/article/promise-chaining/user.json');
  4. let user = await response.json();
  5. // 读取 github 用户信息
  6. let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  7. let githubUser = await githubResponse.json();
  8. // 展示头像
  9. let img = document.createElement('img');
  10. img.src = githubUser.avatar_url;
  11. img.className = "promise-avatar-example";
  12. document.body.append(img);
  13. // 等待 3 秒
  14. await new Promise((resolve, reject) => setTimeout(resolve, 3000));
  15. img.remove();
  16. return githubUser;
  17. }
  18. showAvatar();

更加干净和容易阅读了,对吧?比以前好多了。

异步函数 - 图2await 在顶层代码上无效

刚开始使用的人往往会忘记这一点,但是我们不能在顶级代码中使用 await,这属于无效写法。

  1. // 在顶层代码里使用,会发生语法错误
  2. let response = await fetch('/article/promise-chaining/user.json');
  3. let user = await response.json();

因此,我们需要为等待的代码提供一个包装的异步函数,就像上面的例子一样。

异步函数 - 图3await 接受 thenable 对象

promise.then 类似,await 支持与 thenable 对象(就是包含可调用的 then 方法的对象)配合使用。再一次,我们的想法是,一个第三方的对象可能不是一个 Promise,而是与之兼容的:如果支持 .then,就可以用 await

例如,这里的 await 接受 new Thenable(1)

```javascript class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // 1000ms 之后,使用 this.num2 作为参数调用 resolve 函数 setTimeout(() => resolve(this.num 2), 1000); // (*) } };

async function f() { // 等待 1 秒, 然后 result 赋值为 2 let result = await new Thenable(1); alert(result); }

f();

  1. >
  2. > 如果 `await` 后来跟的是一个带有 `.then` 方法的非 Promise 对象,那么它就会使用原生提供的 `resolve``reject` 函数作为参数调用 `.then` 方法。`await` 会一直等到这两个中的一个被调用(也就是上面代码里标记 `(*)` 的地方),然后再处理结果。
  3. > **⚠️异步方法**
  4. >
  5. > 类方法也可以是异步的,只要在前面加 `async` 关键字即可。
  6. >
  7. > 像这样:
  8. >
  9. > ```javascript
  10. class Waiter {
  11. async wait() {
  12. return await Promise.resolve(1);
  13. }
  14. }
  15. new Waiter()
  16. .wait()
  17. .then(alert); // 1

含义是一样的:它确保返回值是一个 Promise,并允许在内部使用 await

错误处理

如果一个 Promise 正常 resolve 了,然后 await promise 就会返回结果。但是一旦 reject 了,就会抛出错误,就像那一行有一个 throw 语句。

代码:

  1. async function f() {
  2. await Promise.reject(new Error("Whoops!"));
  3. }

等同于:

  1. async function f() {
  2. throw new Error("Whoops!");
  3. }

实际情况中,Promise 可能需要一段时间才会 reject。因此 await 会等待,直至错误抛出。

我们可以使用 try..catch 捕获错误,就像处理一个 throw 语句一样:

  1. async function f() {
  2. try {
  3. let response = await fetch('http://no-such-url');
  4. } catch(err) {
  5. alert(err); // TypeError: failed to fetch
  6. }
  7. }
  8. f();

一旦发生错误,控制权就会转入 catch 块中。我们也可以同时封装多行 await

  1. async function f() {
  2. try {
  3. let response = await fetch('/no-user-here');
  4. let user = await response.json();
  5. } catch(err) {
  6. // 捕获发生在 fetch 或 response.json 中发生的错误
  7. alert(err);
  8. }
  9. }
  10. f();

如果没有使用 try..catch,由调用异步函数 f() 产生的 reject 结果,可以在附加的 .catch() 方法中处理。

  1. async function f() {
  2. let response = await fetch('http://no-such-url');
  3. }
  4. // f() 返回了一个 rejected 状态的 Promise
  5. f().catch(alert); // TypeError: failed to fetch // (*)

如果我们忘记在此处添加 .catch 处理方法了,就会得到一个没有处理的 Promise 错误(可以在控制台看到)。我们可以使用在《Promise 链式调用》章节里提到的全局事件处理器来处理。

⚠️async/await 和 promise.then/catch

在使用 async/await 之后,就很少需要 .then 了,因为 await 会为我们等待处理结果;并且我们使用 try..catch 替代了 .catch 方法。这通常(并非总是)更加方便。

但在代码的顶层,当处于任何异步函数外面时,语法层面上不再允许使用 await 关键字,因此在此种清情形下,我们还是使用 .then/catch 方法处理最终的返回结果(成功或失败)。

就像上例的 (*) 处的展示的那样。

⚠️async/await 与 Promise.all 也能照常协作

当我们需要同时等待多个 Promise 的时候,可以将这些 Promise 式的请求包装在 Promise.all 里,并使用 await 等待返回结果。

  1. // 等待多结果的返回(以数组的形式)
  2. let results = await Promise.all([
  3. fetch(url1),
  4. fetch(url2),
  5. ...
  6. ]);

一旦发生错误,就按照正常方式传播:从失败的 Promise 到 Promise.all,然后使用 try..catch 包装调用就能捕捉产生的异常了。

总结

async 关键字用在函数之前,起到两个作用:

  1. 让函数的返回值始终是一个 Promise 对象。

  2. 允许在函数内部使用 await 关键字。

Promise 之前的 await 关键字会让 JavaScript 一直等待 Promise 解决,然后:

  1. 如果发生错误,就会产生异常,等同于在那个地方使用了 throw error

  2. 否则就返回一个结果,并将它赋值给一个变量。

它们共同提供了一个很好的框架来编写易于读写的异步代码。

有了 async/await 之后,我们就很少写 promise.then/catch 了,但我们还是不能忘记他们是基于 Promise 的,因为有时我们还是会用到这些方法(例如,在最外层作用域中)。而且,使用 Promise.all 同时等待多任务执行是一个不错的选择。

练习题

问题

一、使用 async/await 重写“rethrow”例子

下面是《Promise 链式调用》一章中的“rethrow”例子。请使用 async/await 关键字重写这个例子,替换掉之前的 .then/catch 方法。

  1. class HttpError extends Error {
  2. constructor(response) {
  3. super(`${response.status} for ${response.url}`);
  4. this.name = 'HttpError';
  5. this.response = response;
  6. }
  7. }
  8. function loadJson(url) {
  9. return fetch(url)
  10. .then(response => {
  11. if (response.status == 200) {
  12. return response.json();
  13. } else {
  14. throw new HttpError(response);
  15. }
  16. })
  17. }
  18. // 询问用户名,直至输入的是一个有效的用户名
  19. function demoGithubUser() {
  20. let name = prompt("请输入用户名", "iliakan");
  21. return loadJson(`https://api.github.com/users/${name}`)
  22. .then(user => {
  23. alert(`完整用户名: ${user.name}.`);
  24. return user;
  25. })
  26. .catch(err => {
  27. if (err instanceof HttpError && err.response.status == 404) {
  28. alert("该用户不存在,请重新输入.");
  29. return demoGithubUser();
  30. } else {
  31. throw err;
  32. }
  33. });
  34. }
  35. demoGithubUser();

二、使用 async/await 重写

还是《Promise 链式调用》一章中的例子,使用 async/await 替换 .then/catch 方法:

  1. function loadJson(url) {
  2. return fetch(url)
  3. .then(response => {
  4. if (response.status == 200) {
  5. return response.json();
  6. } else {
  7. throw new Error(response.status);
  8. }
  9. })
  10. }
  11. loadJson('no-such-user.json') // (3)
  12. .catch(alert); // Error: 404

答案

一、使用 async/await 重写“rethrow”例子

这里没什么技巧。只要将 demoGithubUser 里的 .catch 替换为 try...catch,并在需要的地方添加 async/await 即可:

  1. class HttpError extends Error {
  2. constructor(response) {
  3. super(`${response.status} for ${response.url}`);
  4. this.name = 'HttpError';
  5. this.response = response;
  6. }
  7. }
  8. function loadJson(url) {
  9. let response = await fetch(url)
  10. if (response.status == 200) {
  11. return response.json();
  12. } else {
  13. throw new HttpError(response);
  14. }
  15. }
  16. // 询问用户名,直至输入的是一个有效的用户名
  17. function demoGithubUser() {
  18. let user
  19. while(true) {
  20. let name = prompt("请输入用户名", "iliakan");
  21. try {
  22. user = await loadJson(`https://api.github.com/users/${name}`)
  23. break // 没产生错误,退出循环
  24. } catch(err) {
  25. if (err instanceof HttpError && err.response.status == 404) {
  26. alert("该用户不存在,请重新输入.");
  27. return demoGithubUser();
  28. } else {
  29. throw err;
  30. }
  31. }
  32. }
  33. alert(`完整用户名: ${user.name}.`);
  34. return user;
  35. }
  36. demoGithubUser();

二、使用 async/await 重写

可以在代码下查看注解:

  1. async function loadJson(url) { // (1)
  2. let response = await fetch(url); // (2)
  3. if (response.status == 200) {
  4. let json = await response.json();
  5. return json;
  6. } else {
  7. throw new Error(response.status);
  8. }
  9. }
  10. loadJson('no-such-user.json') // (4)
  11. .catch(alert); // Error: 404

注解:

  1. 函数 loadUrl 成为 async 的了。

  2. 所有内部的 .then 替换成了 await

  3. 我们可以 return response.json() 而不是等待它,像这样:

  1. if (response.status == 200) {
  2. return response.json(); // (3)
  3. }

这样的话,外部代码就要 await 直到 Promise 对象被 resolve 了。本例中,这并不重要。

  1. loadJson 中抛出的错误被 .catch 方法处理。这里,我们不能使用 await loadJson(...),因为不是处在 async 函数中。

(完)