顶层 await(top-level await)

ES 提案 top-level await (opens new window) 允许开发者在 async 函数外部使用 await 字段。它就像巨大的 async 函数,原因是 import 它们的模块会等待它们开始执行它的代码。

过去

async/await 首次引用时,尝试在 async 函数外部使用 await 的结果是产生 SyntaxError。大多数开发者使用立即执行异步函数表达式的方式来使用该功能。

await Promise.resolve(console.log('🎉'));
// → SyntaxError: await is only valid in async function
(async function () {
  await Promise.resolve(console.log('🎉'));
  // → 🎉
})();

现在

随着顶层 await 的支持,下面的代码可以替换 modules (opens new window) 中的常用代码:

await Promise.resolve(console.log('🎉'));
// → 🎉

注意:顶层 await 仅能工作在模块的顶层。在 class 代码块或非 async 函数不支持。

何时使用

这些情况借鉴自 spec proposal repository (opens new window)

动态依赖导入

const strings = await import(`/i18n/${navigator.language}`);

这允许在模块的运行时环境中确认依赖项。在像开发环境/生产环境切换,国际化,环境切换等等情况时非常有用。

资源初始化

const connection = await dbConnector();

这允许模块申请资源,同时也可以在模块不能使用时抛出错误。

依赖回退

下面的例子希望从 CDN A 加载 JavaScript 库,如果它加载失败 CDN B 将是备份选择:

let jQuery;
try {
  jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.example.com/jQuery');
}

模块执行顺序

JavaScript 中一个使用 await 的巨大改变是模块树执行顺序。JavaScript 引擎在 post-order traversal(后顺序遍历) (opens new window) 中执行模块:先从模块树左侧子树开始,模块被执行,导出它们的绑定,然后同级也被执行,接着执行父级。该算法递归运行,直到执行模块树的根。

在顶层 await 之前,此顺序始终是同步的和确定性的:在代码的多次运行之间,可以保证代码树以相同的顺序执行。await 到达顶层后,就存在相同的保证,除非你不使用顶层 await

这里是在模块中使用顶层 await 时发生的事:

  1. 执行当前模块直到 await promise 完成状态。
  2. 执行父模块直到子模块执行完 await,包括所有的同级模块执行完,并导出绑定。
  3. 假设代码树中没有周期或其它 await promise,对于同级模块,包括同级的父模块,将可能在相同的同步顺序中继续执行。
  4. await promise 完成后,被调用的模块将继续执行 await
  5. 只要没有其他 await promise ,父模块和子树将继续以同步顺序执行。

顶层 await 能在 DevTools 中执行?

情况确实这样!在 Chrome DevTools (opens new window), Node.js (opens new window) 和 Safari 的 REPL 中支持顶层 await 有一段时间了。但是该方法还不是标准并且限制 REPL!它明确来自 top-level await 提案。生产代码的测试依赖顶层 await 的标准提案的语义,认真测试你的实际应用,不能仅仅依靠 DevTools 或者 Node.js 的 REPL!

译注:REPLread–eval–print loop,译作 “读取-求值-输出”循环,了解更多查看 wikipedia (opens new window)

顶层 await 是臭脚枪吗?

你也许已经看到 Rich Harris (opens new window)Top-level await 臭名昭著的问题 (opens new window),它最初概述了许多有关顶层 await 的问题,并提出 JavaScript 语言不要实现该功能。await 的一些具体的问题是:

  • 顶层 await 会阻断执行。
  • 顶层 await 会阻断资源请求。
  • CommonJS 模块没有确定如何实现。

stage 3 的提案解决了这些问题:

  • 同级之间可以执行,最终不会阻断。
  • 顶层 await 发生在模块图的执行阶段。至此,所有资源均已获取并链接。没有阻塞获取资源的风险。
  • 顶层 await 仅限于 ES 模块。明确不支持脚本或 CommonJS 模块。

作为新的语言功能,总会有一些不可意料的行为。举例来说,使用顶层 await 时,循环模块依赖关系可能会导致死锁。

没有顶层 await 时,JavaScript 开发者经常使用 async 立即执行函数表达式仅仅是为了使用 await。不幸的是,这种模式导致图形执行和应用程序的静态可分析性的确定性降低。由于这些原因,缺少顶层 await 被认为比该功能带来的危害有更高的风险。

支持顶层 await