Top Level Await
Reference Materials
Top-Level Await Stage 3 Draft Proposal-May 28, 2021
Top-Level Await Proposal GitHub-May 28, 2021
Standard Proposal
倡导者:Myles Borins, Yulia Startsev
作者:Myles Borins, Yulia Startsev, Daniel Ehrenberg, Guy Bedford, Ms2ger 等
状态:第 4 阶段
Synopsis
TLA
使模块能够像大型异步函数一样工作:
通过 TLA
,ECMAScript Module(ESM)
可以等待资源加载完成,这会导致导入这些 TLA
模块的其他任意模块在开始执行 TLA
模块的主体代码之前需要等待 TLA
模块中的异步模块执行完成。
Motivation
Limitations on IIAFEs
在 TLA
提案提出之前,await
只能在异步函数(async
)内部使用。也就是说模块的顶层作用域中若存在 await
,就必须将这些与 await
相关的逻辑代码封装在一个异步函数中执行:
// awaiting.mjs
import { process } from './some-module.mjs';
let output;
async function main() {
const dynamic = await import(computedModuleSpecifier);
const data = await fetch(url);
output = process(dynamic.default, data);
}
main();
export { output };
当然,上述的模式也直接立即执行。该模式被称为立即执行异步函数表达式(IIAFE: Immediately Invoked Async Function Expression
),是对 IIFE
习语的一种变体:
// awaiting.mjs
import { process } from './some-module.mjs';
let output;
(async () => {
const dynamic = await import(computedModuleSpecifier);
const data = await fetch(url);
output = process(dynamic.default, data);
})();
export { output };
当模块加载被设计为将来某个时刻需要执行工作时,这种模式是合适的。但是,这个模块导出的内容可能在异步函数还未完成之前就其他逻辑访问:
如果另一个模块导入这个模块,它可能会看到 output
是 undefined
。或者在初始化为 process
的返回值之后才能看到它,这取决于访问发生的时间!例如:
// usage.mjs
import { output } from './awaiting.mjs';
export function outputPlusValue(value) {
return output + value;
}
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);
Workaround: Export a Promise to represent initialization
在没有 TLA
特性的情况下,可以从模块中导出一个 Promise
,等待这个 Promise
完成后其他依赖的模块就知道导出内容已经就绪完毕。例如,上面的模块可以这样写:
// awaiting.mjs
import { process } from './some-module.mjs';
let output;
export default (async () => {
const dynamic = await import(computedModuleSpecifier);
const data = await fetch(url);
output = process(dynamic.default, data);
})();
export { output };
然后,模块可以像如下返回被其他模块所消费:
// usage.mjs
import promise, { output } from './awaiting.mjs';
export function outputPlusValue(value) {
return output + value;
}
promise.then(() => {
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);
});
Legacy Problems
但是这种方式仍然存在一些问题:
- 每个人都需要学习一个特定的协议来找到正确的
Promise
,等待Promise
完成后才可以安全访问导出的数据。 - 若消费者忘记遵循这个协议,那么可能存在因竞态条件而导致的潜在问题。例如,
output
的值有时候可以正常被访问到,有时候却不能。 - 在深层模块依赖结构中,由于
Promise
具备传染性,需要将Promise
在链条的每一步都显式地向上传递。
Avoiding the race through significant additional dynamism
为了避免在访问导出内容之前忘记等待导出的 Promise
所带来的风险,模块可以选择导出一个 Promise
,这个 Promise
解析后会返回包含所有导出内容的对象:
// awaiting.mjs
import { process } from "./some-module.mjs";
export default (async () => {
const dynamic = await import(computedModuleSpecifier);
const data = await fetch(url);
const output = process(dynamic.default, data);
return { output };
})();
// usage.mjs
import promise from "./awaiting.mjs";
export default promise.then(({output}) => {
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000));
return { outputPlusValue };
});
虽然这种模式有时在 Stack Overflow
上被推荐给遇到此类问题的开发者,但这并不是理想的解决方案。这种方式要求对相关的源代码进行广泛的重组,使其变得更加动态化,并且需要将大量模块代码放入 .then()
回调中才能安全使用导入的数据。
与 ES2015
模块相比,这在静态分析性、可测试性、人体工程学等方面都有明显的倒退。若项目中存在 await
的深层依赖模块,那么需要重组所有依赖它的模块来实现上述的模式。
Solution: Top-level await
TLA
让我们可以依靠模块系统本身来处理所有这些 Promise
,并确保一切都能很好地协调。上面的例子可以简单地写成:
// awaiting.mjs
import { process } from './some-module.mjs';
const dynamic = import(computedModuleSpecifier);
const data = fetch(url);
export const output = process((await dynamic).default, await data);
// usage.mjs
import { output } from './awaiting.mjs';
export function outputPlusValue(value) {
return output + value;
}
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);
在 awaiting.mjs
中的 await
完成之前,usage.mjs
中的 主逻辑 都不会执行,所以竞态条件在设计上就被避免了。这是对现有 ES
模块行为的扩展 - 如果 awaiting.mjs
不使用 TLA
,那么在它加载完成且其所有语句执行完之前,usage.mjs
中的 主逻辑 也不会执行。
Use Cases
Dynamic Dependency Pathing
const strings = await import(`/i18n/${navigator.language}`);
这允许模块使用运行时的值来确定依赖关系。这对于开发/生产环境分离、国际化、环境分离等非常有用。
Resource Initialization
const connection = await dbConnector();
类似将模块看作资源,若资源无法正常使用,模块则抛出异常。
Dependency Fallbacks
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
WebAssembly Modules
WebAssembly
模块在逻辑上是通过异步方式来 "编译" 和 "实例化" 的,这基于它们的导入方式。有些 WebAssembly
实现在这两个阶段都会做一些重要的工作,这些重要的工作最好能转移到另一个线程中。为了与 javascript
模块系统集成,它们需要做等同于 TLA
的事情。详细信息可参考 the WebAssembly ESM integration proposal
。
Semantics As Desugaring
目前,一个模块在其依赖执行完所有语句之前,导入操作不会被认定为完成,模块的 主逻辑 也不能运行。引入 await
后,上述的特征依旧保持不变:主逻辑 的执行必须等待所有依赖模块执行完成后才执行。
一种理解方式是把它想象成每个模块都导出一个 Promise
,在所有导入语句之后但在模块的其余部分之前,所有这些 Promise
都被等待:
import { a } from './a.mjs';
import { b } from './b.mjs';
import { c } from './c.mjs';
console.log(a, b, c);
大致等同于:
import { a, promise as aPromise } from './a.mjs';
import { b, promise as bPromise } from './b.mjs';
import { c, promise as cPromise } from './c.mjs';
export const promise = Promise.all([aPromise, bPromise, cPromise]).then(
() => {
console.log(a, b, c);
}
);
模块 a.mjs
、b.mjs
和 c.mjs
都会按顺序执行到它们遇到的第一个 await
;然后我们等待它们全都恢复并完成执行,才继续执行。
TLA Landing Status
以如下代码为例来探索 TLA
在各个构建工具中处理的表现:
import { a } from './a';
import { b } from './b';
import { sleep } from './utils';
await sleep(1000);
console.log(a, b);
console.timeEnd('TLA');
import { sleep } from './utils';
console.time('TLA');
await sleep(1000);
export const a = 124;
import { sleep } from './utils';
await sleep(1000);
export const b = 124;
export const sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
对于上述例子来说,若通过 ESM Bundlers
(i.e. rollup
、esbuild
、bun
、rolldown
)产物见如下:
const sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
console.time('TLA');
await sleep(1000);
const a = 124;
await sleep(1000);
const b = 124;
await sleep(1000);
console.log(a, b);
console.timeEnd('TLA');
// utils.js
var sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
// a.js
console.time('TLA');
await sleep(1e3);
var a = 124;
// b.js
await sleep(1e3);
var b = 124;
// main.js
await sleep(1e3);
console.log(a, b);
console.timeEnd('TLA');
// utils.js
var sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
// a.js
console.time('TLA');
await sleep(1000);
var a = 124;
// b.js
await sleep(1000);
var b = 124;
// main.mjs
await sleep(1000);
console.log(a, b);
console.timeEnd('TLA');
//#region src/tla/utils.js
const sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
//#endregion
//#region src/tla/a.js
console.time('TLA');
await sleep(1e3);
const a = 124;
//#endregion
//#region src/tla/b.js
await sleep(1e3);
const b = 124;
//#endregion
//#region src/tla/main.js
await sleep(1e3);
console.log(a, b);
console.timeEnd('TLA');
//#endregion
Relevant Cases
bun
bun
会原封不动的将TLA
编译到产物中去,同样也没有考虑兼容性,只考虑了现代浏览器的运行:rolldown
在其 官方文档 中也做了相应说明:
At this point, the principle of supporting TLA in rolldown is: we will make it work after bundling without preserving 100% semantic as the original code.
Current rules are:
- If your input contains TLA, it could only be bundled and emitted with esm format.
- require TLA module is forbidden.
可以看出
rolldown
现阶段并未实现完整的TLA
语义,与其他的ESM Bundlers
一样,最终保持串行加载async module
。
由上可知,对于常见的 ESM Bundlers
(i.e. rollup
、esbuild
、bun
、rolldown
)来说,最终产物仅仅只是按照依赖顺序进行平铺处理,并没有专门针对 ES2022
新特性(TLA
)的运行时进行处理,最后输出的产物并没有做到并行加载 async module
,仅仅只是串行加载 async module
,这改变了 TLA
的语义。
根据提案我们可以将上述包含 TLA
模块进行如下方式转译:
import { _TLAPromise as _TLAPromise_1, a } from './a';
import { _TLAPromise as _TLAPromise_2, b } from './b';
import { sleep } from './utils';
Promise.all([_TLAPromise_1(), _TLAPromise_2()])
.then(async () => {
await sleep(1000);
console.log(a, b);
console.timeEnd('TLA');
})
.catch(e => {
console.log(e);
});
import { sleep } from './utils';
console.time('TLA');
export const _TLAPromise = async () => {
await sleep(1000);
};
export const a = 124;
import { sleep } from './utils';
export const _TLAPromise = async () => {
await sleep(1000);
};
export const b = 124;
转译后再通过 ESM Bundlers
(i.e. rollup
、esbuild
、bun
)进行打包,产物如下:
const sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
console.time('TLA');
const _TLAPromise$1 = async () => {
await sleep(1000);
};
const a = 124;
const _TLAPromise = async () => {
await sleep(1000);
};
const b = 124;
Promise.all([_TLAPromise$1(), _TLAPromise()])
.then(async () => {
await sleep(1000);
console.log(a, b);
console.timeEnd('TLA');
})
.catch(e => {
console.log(e);
});
// utils.js
var sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
// a.js
console.time('TLA');
var _TLAPromise = async () => {
await sleep(1e3);
};
var a = 124;
// b.js
var _TLAPromise2 = async () => {
await sleep(1e3);
};
var b = 124;
// TLA.js
Promise.all([_TLAPromise(), _TLAPromise2()])
.then(async () => {
await sleep(1e3);
console.log(a, b);
console.timeEnd('TLA');
})
.catch(e => {
console.log(e);
});
// utils.js
var sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
// a.js
var promise = async () => {
await sleep(1000);
};
var a = 124;
var _TLAPromise = promise;
// b.js
var promise2 = async () => {
await sleep(1000);
};
var b = 124;
var _TLAPromise2 = promise2;
// TLA.js
console.time('TLA');
Promise.all([_TLAPromise(), _TLAPromise2()])
.then(() => {
console.log(a, b);
console.timeEnd('TLA');
})
.catch(e => {
console.log(e);
});
//#region src/tla/utils.js
const sleep = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
//#endregion
//#region src/tla/a.js
console.time('TLA');
const _TLAPromise$1 = async () => {
await sleep(1e3);
};
const a = 124;
//#endregion
//#region src/tla/b.js
const _TLAPromise = async () => {
await sleep(1e3);
};
const b = 124;
//#endregion
//#region src/tla/main.js
Promise.all([_TLAPromise$1(), _TLAPromise()])
.then(async () => {
await sleep(1e3);
console.log(a, b);
console.timeEnd('TLA');
})
.catch(e => {
console.log(e);
});
//#endregion
此时 ESM Bundlers
处理 TLA
模块逻辑遵循了 TLA
规范。这也是 vite-plugin-top-level-await
插件所做的事,暂时缓解了 ESM Bundlers
无法正确处理 TLA
规范的问题。
Tools With TLA Features
webpack
:最早实现
TLA
规范的构建工具是webpack
,仅需确保experiments.topLevelAwait
配置项为true
webpack
版本5.83.0
开始,默认启用此功能。且
TLA
为esm
模块,那么就可以正常编译TLA
。node
在
esm
项目的运行时实现了TLA
规范。但本质上node
的运行时与通用ESM Bundlers
是不一样的,前者并没有执行打包处理,运行时的行为与浏览器有点类似。browsers
ToolChain Environment Timing Summary tsc
Node.js node esm/a.js 0.03s user 0.01s system 4% cpu 1.047 total b、c 的执行是并行的 tsc
Chrome b、c 的执行是并行的 es bundle
Node.js node out.js 0.03s user 0.01s system 2% cpu 1.546 total b、c 的执行是串行的 es bundle
Chrome b、c 的执行是串行的 Webpack (iife)
Node.js node dist/main.js 0.03s user 0.01s system 3% cpu 1.034 total b、c 的执行是并行的 Webpack (iife)
Chrome b、c 的执行是并行的
Summary
虽然 rollup
/ esbuild
/ bun
/ rolldown
等 esm bundlers
工具可以将包含 TLA
的模块成功编译成 es bundle
,但是捆绑后的语义并不符合 TLA
规范,仅仅只是平铺了 TLA
模块,导致原本可以并行执行的 TLA
模块以串型方式执行。
webpack
通过编译到 iife
,再加上复杂的 webpack TLA Runtime
,模拟了 TLA
的语义。换句话说,在打包这件事上,webpack
看起来是唯一一个能够相对正确地模拟 TLA
语义的 bundler
。
The Principle Of Implementing TLA Specification In Webpack
通过 webpack
来构建 TLA
模块,配置信息如下:
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default {
entry: './src/TLA.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'production',
experiments: {
// 从 `webpack` 版本 5.83.0 开始,默认启用此功能
topLevelAwait: true
},
optimization: {
minimize: false
}
};
可以看到输出的产物信息 webpack-tla-output.js(包含代码注释)。
Summary
TLA
模块具有传染性,TLA
模块的所有依赖方模块及依赖方的所有祖先模块也均传染为 TLA
模块。TLA
模块执行时,与往常一样(require
or import
)一样会 DFS
所有的 子依赖模块。不同的是对于 TLA
模块会通过 __webpack_require__.a
来进行特殊初始化,确保当所有的子 TLA
模块均 resolve
完成后才会执行当前模块的 resolve
操作,当前模块 resolve
完成后就可以继续执行当前模块后续的 主逻辑。
本质上 webpack
实现 TLA
的原理,是模拟类似以下的 流程,也是上述提到的 semantics-as-desugaring 的实现。
import { a } from './a.mjs';
import { b } from './b.mjs';
import { c } from './c.mjs';
console.log(a, b, c);
import { a, promise as aPromise } from './a.mjs';
import { b, promise as bPromise } from './b.mjs';
import { c, promise as cPromise } from './c.mjs';
export const promise = Promise.all([aPromise, bPromise, cPromise]).then(
() => {
console.log(a, b, c);
}
);
TLA
模块向上暴露 promise
,目的是为了让依赖方模块集合能确切了解 TLA
模块是否完成 await
操作,通过 Promise.all
确保所有的依赖方模块集合(包含 TLA
模块和非 TLA
模块),TLA
模块能并行执行的同时,非 TLA
模块也能按序解析并执行。
FAQ
What exactly is blocked by a top-level await?
当一个模块导入另一个模块时,导入模块只有在依赖模块的主体逻辑执行完成后才会开始执行其模块主体逻辑。如果依赖模块遇到顶层 await
,那么这个 await
必须完成后,导入模块的主体逻辑才会开始执行。
Why doesn't top-level await block the import of an adjacent module?
如果一个模块想声明自己依赖于另一个模块(为了等待那个模块完成其顶层 await
语句后再执行模块体逻辑),它可以通过 import
语句声明这个依赖关系。
在以下情况中,打印顺序将是 X1 -> Y -> X2
,因为在顺序上"先"导入一个模块并不会创建隐式依赖关系。
import './x.mjs';
import './y.mjs';
console.log('X1');
await new Promise(r => setTimeout(r, 1000));
console.log('X2');
console.log('Y');
需要明确,声明依赖关系是为了提高 并行 的可能性:大多数会因为顶层 await
而阻塞的设置工作(例如,上面提到的所有用例)都可以与其他不相关模块的设置工作并行进行。当部分工作可能高度并行化(例如,网络请求)时,尽可能早地把这些任务排队就显得非常重要。
Setup Work
设置工作指的是模块加载时需要执行的初始化任务。这通常包括以下操作:
当我们在模块中使用 TLA
(即不在 async
函数内部而是直接在模块顶层作用域中使用 await
语句)时,这隐性告知给 javascript
运行时:
这个设置工作必须完成,模块才能被视为完全加载。
举例说明:
// database.js - 数据库初始化
const config = await loadDatabaseConfig(); // 加载数据库配置
const connection = await establishConnection(config); // 建立连接
export const db = connection;
// api.js - API客户端初始化
const apiKey = await loadApiKey(); // 加载API密钥
const client = await initializeApiClient(apiKey); // 初始化API客户端
export const api = client;
这里的关键见解是:
这些设置操作往往是相互独立的,设置操作之间并没有依赖关系。换句话说,按照上述例子,数据库连接不需要等待 API
客户端初始化完成,反之亦然。通过显式声明这些依赖关系,我们可以让 javascript
运行时并行执行这些操作,从而可能节省大量的初始化时间。
What is guaranteed about code execution order?
模块保持与 ES2015
相同的执行启动顺序。如果一个模块遇到 await
,它会让出 控制权,让其他模块按照这个明确规定的顺序初始化自己。
具体来说:
无论是否使用顶层 await
,模块总是按照 ES2015
中确立的后序遍历顺序开始运行:模块体的执行从最深的导入开始,按照到达导入语句的顺序进行。在遇到顶层 await
后,控制权会传递给这个遍历顺序中的下一个模块,或者传递给其他异步调度的代码。
Do these guarantees meet the needs of polyfills?
目前(在没有顶层 await
的世界中),polyfill
是同步的。因此,导入一个 polyfill
(它会修改全局对象)然后导入一个应该受这个 polyfill
影响的模块这种做法,在添加了顶层 await
后仍然有效。不过,如果一个 polyfill
包含顶层 await
,那么依赖它的模块需要导入它才能确保它生效。
Does the Promise.all happen even if none of the imported modules have a top-level await?
如果模块的执行是确定性同步的(也就是说,如果它和它的依赖都不包含顶层 await
),那么这个模块就不会出现在 Promise.all
中。在这种情况下,它会同步运行。
这些语义保留了 ES
模块的当前行为,即在不使用顶层 await
时,求值阶段是完全同步的。这些语义与 Promise
在其他地方的使用有些不同。关于具体示例和进一步讨论,请参见 issue #43 和 issue #47。
How exactly are dependencies waited on? Does it really use Promise.all?
不包含顶层 await
的模块的语义是同步的:整个依赖树按后序执行,一个模块在其所有依赖执行完后就会运行。
同样的语义也适用于包含顶层 await
的模块:一旦包含顶层 await
的模块执行完成,它会触发那些所有依赖都已执行完的依赖模块的同步执行。如果一个模块包含顶层 await
,即使这个 await
并未在运行时被实际执行到,整个模块也会被视为"异步的",就像它是一个大的异步函数一样。
因此,任何在它完成后运行的代码都是在 Promise
回调中。然而,从这里开始,如果有多个模块依赖于它,并且这些模块不包含顶层 await
,那么它们将同步运行,中间不会有任何 Promise
相关的工作。
Does top-level await increase the risk of deadlocks?
顶层 await
确实创造了一种新的死锁机制,但这个提案的倡导者认为这个风险是值得的,原因如下:
- 已经存在许多方式可以创造死锁或者阻止程序继续执行,而开发者工具可以帮助调试它们
- 所有考虑过的确定性死锁预防策略都过于宽泛,会阻止合适的、实际的、有用的模式
Existing Ways to block progress
无限循环
jsfor (const n of primes()) { console.log(`${n} is prime}`); }
无限递归
jsconst fibonacci = n => (n ? fibonacci(n - 1) : 1); fibonacci(Infinity);
Atomics.wait
jsAtomics.wait(shared_array_buffer, 0, 0);
Atomics
允许通过等待一个永远不会改变的索引来阻塞程序进展。export function then
js// a.mjs export function then(f, r) {}
js// main.mjs async function start() { const a = await import('a'); console.log(a); }
导出一个
then
函数允许阻塞import()
。
Summary
确保程序持续进展是一个更大的问题
Rejected deadlock prevention mechanisms
在设计顶层 await
时,一个潜在的问题空间是帮助检测和预防可能发生的死锁形式。例如,在循环动态导入中使用 await
可能会在模块图执行中引入死锁。
以下关于死锁预防的讨论将基于这个代码示例:
<script type="module" src="a.mjs"></script>
await import('./b.mjs');
await import('./a.mjs');
方案一:返回部分填充的模块记录在 b.mjs
中,即使 a.mjs
还没有完成,也立即解析 Promise
,以避免死锁。
方案二:在使用未完成模块时抛出异常在 b.mjs
中,当导入 a.mjs
时因为该模块尚未完成而拒绝 Promise
,以防止死锁。
案例分析:竞争导入同一个模块这两种策略在考虑多段代码可能想要动态导入同一个模块时都会遇到问题。这种多重导入通常不会造成任何竞争或需要担心的死锁。然而,上述两种机制都无法很好地处理这种情况:一个会拒绝 Promise
,另一个则无法等待被导入的模块完成初始化。
Summary
没有可行的死锁避免策略
Will top-level await work in transpilers?
在最大可能的范围内可以。广泛使用的 commonjs (cjs)
模块系统不直接支持顶层 await
,所以任何针对它的转译策略都需要调整。然而,在这个背景下,我们基于几个 javascript
模块系统作者(包括转译器作者)的反馈和经验,对顶层 await
的语义做了几处调整。这个提案的目标是在这样的环境中也能实现。
Without this proposal, module graph execution is synchronous. Does this proposal maintain developer expectations that such loading be synchronous?
在最大可能的范围内是的。当一个模块包含顶层 await(即使这个 await 在运行时没有被执行到),这就不是同步的了,至少需要经过一次 Promise 任务队列。然而,不使用顶层 await 的模块子图会继续以与这个提案之前完全相同的方式同步运行。而且如果几个不使用顶层 await 的模块依赖于一个使用了它的模块,那么这些模块会在异步模块就绪时全部运行,而不会让出控制权给其他工作(既不会让给 Promise 任务队列/微任务队列,也不会让给宿主的事件循环等)。有关使用的逻辑的详细信息,请参见 issue #74。
Should module loading include microtask checkpoints between modules, or yielding to the event loop after modules load?
也许应该!这些模块加载问题是加载性能研究中的一个激动人心的领域,同时也是关于微任务检查点不变量的一个有趣讨论。这个提案不对这些问题采取立场,而是将异步行为留给单独的提案。宿主环境可能会以实现这些功能的方式包装模块,而顶层 await
规范机制可以用来协调这些事情。未来在 TC39
或宿主环境中的提案可能会添加额外的微任务检查点。相关讨论请参见 whatwg/html#4400。
Would top-level await work in web pages?
是的。关于如何集成到 HTML 规范中的详细信息已在 whatwg/html#4352 中提出。
History
async/await
提案 最初于 2014.01
提交给委员会。在 2014.04
月的讨论中,决定在模块目标中保留 await
关键字,以便将来实现顶层 await
。在 2015.07
月,async/await
提案晋升到第 2
阶段。在这次会议中,决定推迟顶层 await
的标准化,以避免阻碍当前提案,因为顶层 await
需要"与加载器一起设计"。
自决定推迟标准化顶层 await
以来,它在委员会讨论中多次被提及,主要是为了确保它在语言中仍然是可行的。
在 2018.05
月,这个提案在 TC39
的流程中达到第 2
阶段,许多设计决策(特别是是否阻塞"兄弟"模块的执行)在第 2
阶段期间被讨论。
Implementations
- V8 v8.9
- SpiderMonkey 通过 javascript.options.experimental.top_level_await 标志启用
- JavaScriptCore
- webpack 5.0.0