顶层 await(top-level await)

ES 提案 top-level await 允许开发者在 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 中的常用代码:

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

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

何时使用

这些情况借鉴自 spec proposal repository

动态依赖导入

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(后顺序遍历) 中执行模块:先从模块树左侧子树开始,模块被执行,导出它们的绑定,然后同级也被执行,接着执行父级。该算法递归运行,直到执行模块树的根。

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

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

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

顶层 await 能在 DevTools 中执行?

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

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

顶层 await 是臭脚枪吗?

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

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

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

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

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

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

支持顶层 await