从 v9.1
开始,在 V8
中默认启用顶级 await
,并且在没有 --harmony-top-level-await
配置的情况下也是可以用的。
在
Blink
渲染引擎中,v89
版本默认情况下已经启用了顶层await
什么是顶层 await
在以前,我们必须在一个 async
函数中才能使用 await
,如果直接在一个模块最外层使用 await
会抛出 SyntaxError
异常,为此我们通常会在外面包裹一个立即执行函数:
await Promise.resolve(console.log('?'));
// → SyntaxError: await is only valid in async function
(async function() {
await Promise.resolve(console.log('?'));
// → ?
}());
现在我们可以在整个模块的最外层直接使用 await
,这让我们的整个模块看一来就像一个巨大的 async
函数。
await Promise.resolve(console.log('?'));
// → ?
注意,顶层
await
仅仅是允许我们在模块的最外层允许使用await
,传统的script
标签或非async
函数均不能直接使用。
为什么要引入顶层 await
下面举一个我们实际开发中可能会遇到的一个问题:
工具库模块
在一个工具库模块中,我们导出了两个函数:
代码语言:javascript复制//------ library.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diagonal(x, y) {
return sqrt(square(x) square(y));
}
中间件
在一个中间件中,我们每次需要等待一些事情执行完,再执行工具库导出的两个函数:
代码语言:javascript复制//------ middleware.js ------
import { square, diagonal } from './library.js';
console.log('From Middleware');
let squareOutput;
let diagonalOutput;
// IIFE
(async () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
})();
function delay(delayInms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(console.log('❤️'));
}, delayInms);
});
}
export {squareOutput,diagonalOutput};
主程序
在主程序中,我们要调用中间件导出的两个值,但是我们并不能直接立刻拿到结果,而是必须自己写一个异步等待的代码才能拿到:
代码语言:javascript复制//------ main.js ------
import { squareOutput, diagonalOutput } from './middleware.js';
console.log(squareOutput); // undefined
console.log(diagonalOutput); // undefined
console.log('From Main');
setTimeout(() => console.log(squareOutput), 2000);
//169
setTimeout(() => console.log(diagonalOutput), 2000);
//13
解决方案
这时,我们可能就会用到我们的主角,顶层 await:
代码语言:javascript复制
//------ middleware.js ------
import { square, diagonal } from './library.js';
console.log('From Middleware');
let squareOutput;
let diagonalOutput;
//使用顶层 await
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
function delay(delayInms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(console.log('❤️'));
}, delayInms);
});
}
export {squareOutput,diagonalOutput};
//------ main.js ------
import { squareOutput, diagonalOutput } from './middleware.js';
console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');
setTimeout(() => console.log(squareOutput), 2000);// 169
setTimeout(() => console.log(diagonalOutput), 2000); // 13
在以上的代码中, main.js
会等待 middleware.js
中的 await promise
被 resolve
后,才会执行它的代码,是不是非常方便!
其他应用场景
动态依赖导入
这允许在模块的运行时环境中确认依赖项,在开发/生产环境切换、国际化等场景中非常有用。
代码语言:javascript复制const strings = await import(`/i18n/${navigator.language}`);
资源初始化
代码语言:javascript复制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
时:
- 等待 await 执行完成后才会执行当前模块。
- 子模块执行完 await,并且包括所有的同级模块执行完,并导出绑定,才会执行父模块。
- 假设代码树中没循环或者其它 await ,同级模块和父模块,会以相同的同步顺序继续执行。
- 在 await 完成后,被调用的模块将继续执行 await。
- 只要没有其他 await ,父模块和子树将继续以同步顺序执行。
你可能会考虑的一些问题
- 顶层 await 会阻断执行?
- 同级之间可以执行,最终不会阻断。
- 顶层 await 会阻断资源请求。
- 顶层 await 发生在模块图的执行阶段,此时所有资源均开始链接,没有阻塞获取资源的风险。
- CommonJS 模块没有确定如何实现。
- 顶层 await 仅限于 ES 模块,明确不支持 script 或 CommonJS 模块。