await

基线 广泛可用

此功能非常成熟,可以在许多设备和浏览器版本上运行。它从以下日期开始在浏览器中可用 2017 年 4 月.

await 运算符用于等待 Promise 并获取其完成值。它只能在 异步函数 内部或 模块 的顶层使用。

语法

js
await expression

参数

表达式

一个 Promise,一个 thenable 对象,或任何要等待的值。

返回值

promise 或 thenable 对象的完成值,或者,如果表达式不是 thenable,则表达式的自身值。

异常

如果 promise 或 thenable 对象被拒绝,则抛出拒绝原因。

描述

await 通常用于通过将 Promise 作为 表达式 传递来解包 promise。使用 await 会暂停其周围 async 函数的执行,直到 promise 解决(即完成或拒绝)。当执行恢复时,await 表达式的值将变为完成的 promise 的值。

如果 promise 被拒绝,await 表达式将抛出拒绝值。包含 await 表达式的函数将 出现在错误的堆栈跟踪中。否则,如果拒绝的 promise 没有被等待或立即返回,则调用者函数将不会出现在堆栈跟踪中。

表达式 的解析方式与 Promise.resolve() 相同:它始终被转换为本机 Promise,然后被等待。如果 表达式

  • 本机 Promise(这意味着 表达式 属于 Promise 或子类,并且 表达式.constructor === Promise):promise 将直接使用并本机等待,无需调用 then()
  • Thenable 对象(包括非本机 promise,polyfill,代理,子类等):使用本机 Promise() 构造函数通过调用对象的 then() 方法并传递一个调用 resolve 回调的处理程序来构造一个新 promise。
  • 非 thenable 值:一个已经完成的 Promise 被构造并使用。

即使使用的 promise 已经完成,异步函数的执行仍然会暂停,直到下一个事件循环。在此期间,异步函数的调用者会恢复执行。 参见下面的示例。

由于 await 仅在异步函数和模块内部有效,而异步函数和模块本身是异步的并返回 promise,因此 await 表达式永远不会阻塞主线程,并且只会延迟实际依赖结果的代码的执行,即 await 表达式之后的任何内容。

示例

等待 promise 完成

如果将 Promise 传递给 await 表达式,它将等待 Promise 完成并返回完成的值。

js
function resolveAfter2Seconds(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function f1() {
  const x = await resolveAfter2Seconds(10);
  console.log(x); // 10
}

f1();

Thenable 对象

Thenable 对象 的解析方式与实际的 Promise 对象相同。

js
async function f() {
  const thenable = {
    then(resolve, _reject) {
      resolve("resolved!");
    },
  };
  console.log(await thenable); // "resolved!"
}

f();

它们也可以被拒绝

js
async function f() {
  const thenable = {
    then(resolve, reject) {
      reject(new Error("rejected!"));
    },
  };
  await thenable; // Throws Error: rejected!
}

f();

转换为 promise

如果该值不是 Promiseawait 会将该值转换为已完成的 Promise,并等待它。只要该值没有可调用的 then 属性,等待的值的标识就不会改变。

js
async function f3() {
  const y = await 20;
  console.log(y); // 20

  const obj = {};
  console.log((await obj) === obj); // true
}

f3();

处理拒绝的 promise

如果 Promise 被拒绝,则会抛出拒绝值。

js
async function f4() {
  try {
    const z = await Promise.reject(30);
  } catch (e) {
    console.error(e); // 30
  }
}

f4();

您可以在等待 promise 之前通过链接一个 catch() 处理程序来处理拒绝的 promise。

js
const response = await promisedFunction().catch((err) => {
  console.error(err);
  return "default response";
});
// response will be "default response" if the promise is rejected

这是基于以下假设:promisedFunction() 从不同步抛出错误,但始终返回一个拒绝的 promise。对于大多数设计良好的基于 promise 的函数,情况就是如此,这些函数通常看起来像

js
function promisedFunction() {
  // Immediately return a promise to minimize chance of an error being thrown
  return new Promise((resolve, reject) => {
    // do something async
  });
}

但是,如果 promisedFunction() 同步抛出错误,则错误不会被 catch() 处理程序捕获。在这种情况下,try...catch 语句是必需的。

顶层 await

您可以在 模块 的顶层(在异步函数之外)单独使用 await 关键字。这意味着使用 await 的子模块的模块将在它们自己运行之前等待子模块执行,同时不会阻塞其他子模块加载。

这是一个使用 Fetch API 的简单模块的示例,并在 export 语句中指定 await。任何包含此模块的模块都将在运行任何代码之前等待获取完成。

js
// fetch request
const colors = fetch("../data/colors.json").then((response) => response.json());

export default await colors;

await 的控制流影响

当代码中遇到 await(无论是在异步函数中还是在模块中),都会执行等待的表达式,而所有依赖表达式值的代码都会被暂停并推入 微任务队列。然后,主线程将释放用于事件循环中的下一个任务。即使等待的值是已经完成的 promise 或不是 promise,也会发生这种情况。例如,考虑以下代码

js
async function foo(name) {
  console.log(name, "start");
  console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// First end
// Second start
// Second middle
// Second end

在这种情况下,两个异步函数实际上是同步的,因为它们不包含任何 await 表达式。三个语句在同一个事件循环中发生。在 promise 术语中,该函数对应于

js
function foo(name) {
  return new Promise((resolve) => {
    console.log(name, "start");
    console.log(name, "middle");
    console.log(name, "end");
    resolve();
  });
}

但是,只要有一个 await,该函数就变为异步函数,后续语句的执行将推迟到下一个事件循环。

js
async function foo(name) {
  console.log(name, "start");
  await console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// Second start
// Second middle
// First end
// Second end

这对应于

js
function foo(name) {
  return new Promise((resolve) => {
    console.log(name, "start");
    resolve(console.log(name, "middle"));
  }).then(() => {
    console.log(name, "end");
  });
}

虽然额外的 then() 处理程序不是必需的,并且可以将处理程序与传递给构造函数的执行器合并,但 then() 处理程序的存在意味着代码将多花费一个事件循环才能完成。await 也是如此。因此,请确保仅在必要时(将 promise 解包为其值)使用 await

其他微任务可以在异步函数恢复之前执行。此示例使用 queueMicrotask() 来演示当遇到每个 await 表达式时如何处理微任务队列。

js
let i = 0;

queueMicrotask(function test() {
  i++;
  console.log("microtask", i);
  if (i < 3) {
    queueMicrotask(test);
  }
});

(async () => {
  console.log("async function start");
  for (let i = 1; i < 3; i++) {
    await null;
    console.log("async function resume", i);
  }
  await null;
  console.log("async function end");
})();

queueMicrotask(() => {
  console.log("queueMicrotask() after calling async function");
});

console.log("script sync part end");

// Logs:
// async function start
// script sync part end
// microtask 1
// async function resume 1
// queueMicrotask() after calling async function
// microtask 2
// async function resume 2
// microtask 3
// async function end

在此示例中,test() 函数始终在异步函数恢复之前调用,因此它们各自安排的微任务始终以交织的方式执行。另一方面,由于 awaitqueueMicrotask() 都安排了微任务,因此执行顺序始终基于调度顺序。这就是为什么 "调用异步函数后的 queueMicrotask()" 日志在异步函数第一次恢复后发生的。

改进堆栈跟踪

有时,当 promise 直接从异步函数返回时,会省略 await

js
async function noAwait() {
  // Some actions...

  return /* await */ lastAsyncTask();
}

但是,考虑 lastAsyncTask 异步抛出错误的情况。

js
async function lastAsyncTask() {
  await null;
  throw new Error("failed");
}

async function noAwait() {
  return lastAsyncTask();
}

noAwait();

// Error: failed
//    at lastAsyncTask

只有 lastAsyncTask 出现在堆栈跟踪中,因为 promise 在已经从 noAwait 返回后被拒绝了——从某种意义上说,promise 与 noAwait 无关。为了改进堆栈跟踪,您可以使用 await 来解包 promise,以便异常被抛入当前函数。然后,异常将立即被包装在一个新的拒绝 promise 中,但在创建错误期间,调用者将出现在堆栈跟踪中。

js
async function lastAsyncTask() {
  await null;
  throw new Error("failed");
}

async function withAwait() {
  return await lastAsyncTask();
}

withAwait();

// Error: failed
//    at lastAsyncTask
//    at async withAwait

与一些普遍的看法相反,return await promise 至少与 return promise 一样快,这是因为规范和引擎对原生 Promise 的解析进行了优化。有一个提案要使 return promise 更快,你还可以阅读关于V8 对异步函数的优化。因此,除了风格原因外,return await 几乎总是更可取的。

规范

规范
ECMAScript 语言规范
# sec-async-function-definitions

浏览器兼容性

BCD 表格只在浏览器中加载

另请参阅