顶层 await

发布于 · 标签:ECMAScript

顶层 await 使开发人员能够在异步函数之外使用 await 关键字。它就像一个大型异步函数,导致其他 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,上面的代码将在 模块 中按预期工作。

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

注意:顶层 await 在模块的顶层有效。不支持经典脚本或非异步函数。

用例 #

这些用例来自 规范提案库

动态依赖路径 #

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');
}

模块执行顺序 #

顶层 await 对 JavaScript 的最大改变之一是模块图中模块的执行顺序。JavaScript 引擎以 后序遍历 的方式执行模块:从模块图的最左侧子树开始,评估模块、导出其绑定并执行其兄弟节点,然后执行其父节点。此算法递归运行,直到执行模块图的根节点。

在顶层 await 之前,此顺序始终是同步且确定的:在代码的多次运行之间,保证图以相同的顺序执行。一旦顶层 await 出现,相同的保证仍然存在,但只有在不使用顶层 await 的情况下才存在。

以下是您在模块中使用顶层 await 时发生的情况

  1. 当前模块的执行将延迟,直到等待的 Promise 解决。
  2. 父模块的执行将延迟,直到调用 await 的子模块及其所有兄弟节点导出绑定。
  3. 兄弟模块和父模块的兄弟节点能够继续以相同的同步顺序执行——假设图中没有循环或其他 awaited Promise。
  4. 调用 await 的模块在 awaited Promise 解决后恢复执行。
  5. 父模块和后续树继续以同步顺序执行,只要没有其他 awaited Promise。

这在 DevTools 中不是已经可以工作了吗? #

确实如此!Chrome DevToolsNode.js 和 Safari Web Inspector 中的 REPL 一直支持顶层 await。但是,此功能是非标准的,仅限于 REPL!它不同于顶层 await 提案,该提案是语言规范的一部分,仅适用于模块。要以完全匹配规范提案语义的方式测试依赖顶层 await 的生产代码,请确保在实际应用程序中进行测试,而不仅仅是在 DevTools 或 Node.js REPL 中测试!

顶层 await 不是一个坑吗? #

也许您已经看到了 Rich Harris 的臭名昭著的 gist,该 gist 最初概述了对顶层 await 的一些担忧,并敦促 JavaScript 语言不要实现此功能。一些具体的担忧是

  • 顶层 await 可能会阻塞执行。
  • 顶层 await 可能会阻塞获取资源。
  • 对于 CommonJS 模块,没有明确的互操作性方案。

提案的第 3 阶段版本直接解决了这些问题

  • 由于兄弟节点能够执行,因此没有明确的阻塞。
  • 顶层 await 发生在模块图的执行阶段。此时,所有资源都已获取并链接。没有阻塞获取资源的风险。
  • 顶层 await 仅限于模块。明确不支持脚本或 CommonJS 模块。

与任何新的语言特性一样,始终存在意外行为的风险。例如,使用顶层 await,循环模块依赖项可能会导致死锁。

没有顶层 await,JavaScript 开发人员通常使用异步立即调用的函数表达式来访问 await。不幸的是,这种模式会导致图执行的确定性降低和应用程序的静态可分析性降低。出于这些原因,缺乏顶层 await 被认为比该特性带来的危害风险更大。

对顶层 await 的支持 #