2022 年 2 月 6 日

Async/await

有一种特殊的语法可以更轻松地处理 Promise,称为“async/await”。它出乎意料地易于理解和使用。

Async 函数

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

async function f() {
  return 1;
}

函数之前的“async”一词表示一个简单的事实:函数始终返回一个 Promise。其他值会自动包装在一个已解决的 Promise 中。

例如,此函数返回一个已解决的 Promise,其结果为 1;让我们测试一下

async function f() {
  return 1;
}

f().then(alert); // 1

…我们可以显式返回一个 Promise,这将是相同的

async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

因此,async 确保函数返回一个 promise,并用它包装非 promise。很简单,对吧?但不仅如此。还有另一个关键字 await,它仅在 async 函数中起作用,而且非常酷。

Await

语法

// works only inside async functions
let value = await promise;

关键字 await 使 JavaScript 等待该 promise 结算并返回其结果。

下面是一个在 1 秒内解决的 promise 的示例

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // wait until the promise resolves (*)

  alert(result); // "done!"
}

f();

函数执行在行 (*) 处“暂停”,并在 promise 结算时恢复,result 成为其结果。因此,上面的代码在一秒后显示“done!”。

让我们强调一下:await 会在 promise 结算之前暂停函数执行,然后使用 promise 结果恢复执行。这不会消耗任何 CPU 资源,因为 JavaScript 引擎可以在此期间执行其他作业:执行其他脚本、处理事件等。

这只是比 promise.then 更优雅的获取 promise 结果的语法。而且,它更易于阅读和编写。

不能在常规函数中使用 await

如果我们尝试在非 async 函数中使用 await,则会出现语法错误

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

如果忘记在函数前加上 async,我们可能会收到此错误。如前所述,await 仅在 async 函数中起作用。

让我们从章节 Promises chaining 中获取 showAvatar() 示例,并使用 async/await 重写它

  1. 我们需要用 await 替换 .then 调用。
  2. 我们还应该使函数成为 async 函数才能使它们起作用。
async function showAvatar() {

  // read our JSON
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // read github user
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // show the avatar
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // wait 3 seconds
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

相当简洁且易于阅读,对吧?比以前好多了。

现代浏览器允许在模块中使用顶级 await

在现代浏览器中,当我们在模块中时,顶级 await 可以正常工作。我们将在文章 Modules, introduction 中介绍模块。

例如

// we assume this code runs at top level, inside a module
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

console.log(user);

如果我们不使用模块,或者必须支持 较旧的浏览器,则有一个通用的方法:包装到一个匿名 async 函数中。

像这样

(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();
await 接受“thenables”

promise.then 类似,await 允许我们使用可 then 的对象(那些具有可调用的 then 方法的对象)。这个想法是第三方对象可能不是一个 promise,但与 promise 兼容:如果它支持 .then,那么它足以与 await 一起使用。

这是一个 Thenable 类的演示;下面的 await 接受它的实例

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // resolve with this.num*2 after 1000ms
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // waits for 1 second, then result becomes 2
  let result = await new Thenable(1);
  alert(result);
}

f();

如果 await 获得一个具有 .then 的非 promise 对象,它将调用该方法,提供内置函数 resolvereject 作为参数(就像它对常规 Promise 执行器所做的那样)。然后 await 等待其中一个被调用(在上面的示例中,它发生在行 (*) 中),然后继续执行结果。

异步类方法

要声明一个异步类方法,只需在它前面加上 async

class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1 (this is the same as (result => alert(result)))

含义是相同的:它确保返回的值是一个 promise 并启用 await

错误处理

如果一个 promise 正常解决,那么 await promise 将返回结果。但在被拒绝的情况下,它会抛出错误,就像在该行有一个 throw 语句一样。

这段代码

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

…与这个相同

async function f() {
  throw new Error("Whoops!");
}

在实际情况下,promise 可能需要一段时间才能拒绝。在这种情况下,在 await 抛出错误之前会有一个延迟。

我们可以使用 try..catch 捕获该错误,就像一个常规的 throw

async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

在发生错误的情况下,控制会跳转到 catch 块。我们还可以包装多行

async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // catches errors both in fetch and response.json
    alert(err);
  }
}

f();

如果我们没有 try..catch,那么由异步函数 f() 的调用生成的 promise 将被拒绝。我们可以附加 .catch 来处理它

async function f() {
  let response = await fetch('http://no-such-url');
}

// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)

如果我们忘记在那里添加 .catch,那么我们会收到一个未处理的 promise 错误(可以在控制台中查看)。我们可以使用全局 unhandledrejection 事件处理程序捕获此类错误,如章节 使用 promise 进行错误处理 中所述。

async/awaitpromise.then/catch

当我们使用 async/await 时,我们很少需要 .then,因为 await 为我们处理了等待。我们可以使用常规的 try..catch 代替 .catch。这通常(但并非总是)更方便。

但在代码的顶级,当我们位于任何 async 函数之外时,我们在语法上无法使用 await,因此添加 .then/catch 来处理最终结果或失败错误是一种正常做法,就像上面示例的行 (*) 中一样。

async/awaitPromise.all 配合得很好

当我们需要等待多个 promise 时,我们可以将它们包装在 Promise.all 中,然后 await

// wait for the array of results
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

在发生错误的情况下,它会像往常一样从失败的 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 而不是 .then/catch 重写章节 Promises chaining 中的此示例代码

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new Error(response.status);
      }
    });
}

loadJson('https://javascript.js.cn/no-such-user.json')
  .catch(alert); // Error: 404

注释在代码下方

async function loadJson(url) { // (1)
  let response = await fetch(url); // (2)

  if (response.status == 200) {
    let json = await response.json(); // (3)
    return json;
  }

  throw new Error(response.status);
}

loadJson('https://javascript.js.cn/no-such-user.json')
  .catch(alert); // Error: 404 (4)

注释

  1. 函数 loadJson 变成 async

  2. 内部的所有 .then 都替换为 await

  3. 我们可以 return response.json() 而不是等待它,如下所示

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

    然后外部代码将不得不 await 该 promise 解析。在我们的例子中,这并不重要。

  4. loadJson 抛出的错误由 .catch 处理。我们不能在那里使用 await loadJson(…),因为我们不在 async 函数中。

在下面你可以找到“rethrow”示例。使用 async/await 而不是 .then/catch 重写它。

并且在 demoGithubUser 中使用循环来摆脱递归:使用 async/await,这变得很容易。

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new HttpError(response);
      }
    });
}

// Ask for a user name until github returns a valid user
function demoGithubUser() {
  let name = prompt("Enter a name?", "iliakan");

  return loadJson(`https://api.github.com/users/${name}`)
    .then(user => {
      alert(`Full name: ${user.name}.`);
      return user;
    })
    .catch(err => {
      if (err instanceof HttpError && err.response.status == 404) {
        alert("No such user, please reenter.");
        return demoGithubUser();
      } else {
        throw err;
      }
    });
}

demoGithubUser();

这里没有技巧。只需在 demoGithubUser 中用 try..catch 替换 .catch,并在需要时添加 async/await

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

async function loadJson(url) {
  let response = await fetch(url);
  if (response.status == 200) {
    return response.json();
  } else {
    throw new HttpError(response);
  }
}

// Ask for a user name until github returns a valid user
async function demoGithubUser() {

  let user;
  while(true) {
    let name = prompt("Enter a name?", "iliakan");

    try {
      user = await loadJson(`https://api.github.com/users/${name}`);
      break; // no error, exit loop
    } catch(err) {
      if (err instanceof HttpError && err.response.status == 404) {
        // loop continues after the alert
        alert("No such user, please reenter.");
      } else {
        // unknown error, rethrow
        throw err;
      }
    }
  }


  alert(`Full name: ${user.name}.`);
  return user;
}

demoGithubUser();

我们有一个名为 f 的“常规”函数。你如何在 f 中调用 async 函数 wait() 并使用它的结果?

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // ...what should you write here?
  // we need to call async wait() and wait to get 10
  // remember, we can't use "await"
}

P.S. 从技术上讲,这项任务非常简单,但对于不熟悉 async/await 的开发人员来说,这个问题很常见。

这是了解其内部工作原理很有帮助的情况。

只需将 async 调用视为 promise,并附加 .then

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // shows 10 after 1 second
  wait().then(result => alert(result));
}

f();
教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入一些代码,请使用 <code> 标签,对于多行代码 - 将其包装在 <pre> 标签中,对于 10 行以上的代码 - 使用沙盒 (plnkrjsbincodepen…)