V8 9.1 正式支持顶层 await !

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

下面举一个我们实际开发中可能会遇到的一个问题:

工具库模块

在一个工具库模块中,我们导出了两个函数:

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

中间件

在一个中间件中,我们每次需要等待一些事情执行完,再执行工具库导出的两个函数:

//------ 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};

主程序

在主程序中,我们要调用中间件导出的两个值,但是我们并不能直接立刻拿到结果,而是必须自己写一个异步等待的代码才能拿到:

//------ 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:


//------ 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 promiseresolve 后,才会执行它的代码,是不是非常方便!

其他应用场景

动态依赖导入

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

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 时:

  • 等待 await 执行完成后才会执行当前模块。
  • 子模块执行完 await,并且包括所有的同级模块执行完,并导出绑定,才会执行父模块。
  • 假设代码树中没循环或者其它 await ,同级模块和父模块,会以相同的同步顺序继续执行。
  • 在 await 完成后,被调用的模块将继续执行 await。
  • 只要没有其他 await ,父模块和子树将继续以同步顺序执行。

你可能会考虑的一些问题

  • 顶层 await 会阻断执行?
    • 同级之间可以执行,最终不会阻断。
  • 顶层 await 会阻断资源请求。
    • 顶层 await 发生在模块图的执行阶段,此时所有资源均开始链接,没有阻塞获取资源的风险。
  • CommonJS 模块没有确定如何实现。
    • 顶层 await 仅限于 ES 模块,明确不支持 script 或 CommonJS 模块。

抖音前端急缺人才,如果你有意向加入我们,或有其他事情想和我交流,欢迎加我的个人微信 ConardLi 和我联系。