6

为什么需要在 JavaScript 中使用顶层 await?

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU4MTA5MjI4Mw%3D%3D&%3Bmid=2247484774&%3Bidx=1&%3Bsn=3322862f5af27143a3a85ff07456d319
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
  • 原文地址: Why Should You Use Top-level Await in JavaScript? [1]

  • 原文作者: Mahdhi Rezvi [2]

  • 译者:Chor

beUzui3.png!mobile

作为一门非常灵活和强大的语言,JavaScript 对现代 web 产生了深远的影响。它之所以能够在 web 开发中占据主导地位,其中一个主要原因就是频繁更新所带来的持续改进。

顶层 await( top-level await )是近年来提案中涉及的新特性。该特性可以让 ES 模块对外表现为一个 async 函数,允许 ES 模块去 await 数据并阻塞其它导入这些数据的模块。只有在数据确定并准备好的时候,导入数据的模块才可以执行相应的代码。

有关该特性的提案目前仍处于 stage 3 阶段,因此我们不能直接在生产环境中使用。但鉴于它将在不久的未来推出,提前了解一下还是大有好处的。

听起来一头雾水没关系,继续往下阅读,我会和你一起搞定这个新特性的。

以前的写法,问题在哪里?

在引入顶层 await 之前,如果你试图在一个 async 函数外面使用 await 关键字,将会引起语法错误。为了避免这个问题,开发者通常会使用立即执行函数表达式(IIFE)

await Promise.resolve(console.log(':heart:'));
//报错

(async () => {
await Promise.resolve(console.log(':heart:'));
//:heart:
})();

然而这只是冰山一角

在使用 ES6 模块化的时候,经常会遇到需要导入导出的场景。看看下面这个例子:

//------ 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(':heart:'));
}, delayInms);
});
}

export {squareOutput,diagonalOutput};

在这个例子中,我们在 library.jsmiddleware.js 之间进行变量的导入导出 (文件名随意,这里不是重点)

如果仔细阅读,你会注意到有一个 delay 函数,它返回的 Promise 会在计时结束之后被 resolve。因为这是一个异步操作(在真实的业务场景中,这里可能会是一个 fetch 调用或者某个异步任务),我们在 async IIFE 中使用 await 以等待其执行结果。一旦 promise 被 resolve,我们会执行从 library.js 中导入的函数,并将计算得到的结果赋值给两个变量。这意味着,在 promise 被 resolve 之前,两个变量都会是 undefined

在代码最后面,我们将计算得到的两个变量导出,供另一个模块使用。

下面这个模块负责导入并使用上述两个变量:

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

运行上面代码,你会发现前两次打印得到的都是 undefined ,后两次打印得到的是 169 和 13。为什么会这样呢?

这是因为,在 async 函数执行完毕之前, main.js 就已经访问了 middleware.js 导出的变量。记得吗?我们前面还有一个 promise 等待被 resolve 呢 ……

为了解决这个问题,我们需要想办法通知模块,让它在准备好访问变量的时候再将变量导入。

解决方案

针对上述问题,有两个广泛使用的解决方案:

1.导出一个 Promise 表示初始化

你可以导出一个 IIFE 并依靠它确定可以访问导出结果的时机。 async 关键字可以异步化一个方法,并相应 返回一个 promise [3] 。因此,下面的代码中, async IIFE 会返回一个 promise。

//------ middleware.js ------
import { square, diagonal } from './library.js';

console.log('From Middleware');

let squareOutput;
let diagonalOutput;

//解决方案
export default (async () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
})();

function delay(delayInms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(console.log(':heart:'));
}, delayInms);
});
}

export {squareOutput,diagonalOutput};

当你在 main.js 中访问导出结果的时候,你可以静待 async IIFE 被 resolve,之后再去访问变量。

//------ main.js ------
import promise, { squareOutput, diagonalOutput } from './middleware.js';

promise.then(()=>{
console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');

setTimeout(() => console.log(squareOutput), 2000);// 169

setTimeout(() => console.log(diagonalOutput), 2000);// 13
})

尽管这个方案可以生效,但它也引入了新的问题:

  • 大家都必须将这种模式作为标准去遵循,而且必须要找到并等待合适的 promise;

  • main.js
    squareOutput
    diagonalOutput
    

为了解决这两个新问题,第二个方案应运而生。

2.用导出的变量去 resolve IIFE promise

在这个方案中,我们不再像之前那样单独导出变量,而是将变量作为 async IIFE 的返回值返回。这样的话, main.js 只需简单地等待 promise 被 resolve,之后直接获取变量即可。

//------ middleware.js ------
import { square, diagonal } from './library.js';

console.log('From Middleware');

let squareOutput;
let diagonalOutput;

export default (async () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
return {squareOutput,diagonalOutput};
})();

function delay(delayInms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(console.log(':heart:'));
}, delayInms);
});
}

//------ main.js ------

import promise from './middleware.js';

promise.then(({squareOutput,diagonalOutput})=>{
console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');

setTimeout(() => console.log(squareOutput), 2000);// 169

setTimeout(() => console.log(diagonalOutput), 2000);// 13
})

但这个方案有其自身的复杂性存在。

根据提案的说法,“这种模式的不良影响在于,它要求对相关数据进行大规模重构以使用动态模式;同时,它将模块的大部分内容放在 .then() 的回调函数中,以使用动态导入。从静态分析、可测试性、工程学以及其它角度来讲,这种做法相比 ES2015 的模块化来说是一种显而易见的倒退”。

顶层 Await 是如何解决上述问题的?

顶层 await 允许我们让模块系统去处理 promise 之间的协调关系,从而让我们这边的工作变得异常简单。

//------ 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(':heart:'));
}, 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

middleware.js 中的 await promise 被 resolve 之前, main.js 中任意一条语句都不会执行。与之前提及的解决方案相比,这个方法要简洁得多。

注意

必须注意的是,顶层 await 只在 ES 模块中生效。此外,你必须要显式声明模块之间的依赖关系,才能让顶层 await 像预期那样生效。提案仓库中的这段代码就很好地说明了这个问题:

// x.mjs
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
// y.mjs
console.log("Y");
// z.mjs
import "./x.mjs";
import "./y.mjs";
//X1
//Y
//X2

这段代码打印的顺序并不是预想中的 X1,X2,Y 。这是因为 xy 是独立的模块,互相之间没有依赖关系。

推荐你阅读一下 文档问答 [4] ,这样会对这个顶层 await 这个新特性有更加全面的了解。

试用

V8

你可以按照 文档 [5] 所说的,尝试使用顶层 await 特性。

我使用的是 V8 的方法。找到你电脑上 Chrome 浏览器的安装位置,确保关闭浏览器的所有进程,打开命令行运行如下命令:

chrome.exe --js-flags="--harmony-top-level-await"

这样,Chrome 重新打开后将开启对于顶层 await 特性的支持。

当然,你也可以在 Node 环境测试。阅读 这个指南 [6] 获取更多细节。

ES 模块

确保在 script 标签中声明该属性: type="module"

<script type="module" src="./index.js" >
</script>

需要注意的是,和普通脚本不一样,声明模块化之后的脚本会受到 CORS 策略的影响,因此你需要通过服务器打开该文件。

应用场景

以下是 提案 [7] 中讲到的相关用例:

动态的依赖路径

const strings = await import(`/i18n/${navigator.language}`);

允许模块使用运行时的值去计算得到依赖关系。这对生产/开发环境的区分以及国际化工作等非常有效。

资源初始化

const connection = await dbConnector();

这有助于把模块看作某种资源,同时可以在模块不存在的时候抛出错误。错误可以在下面介绍的后备方案中得到处理。

依赖的后备方案

下面的例子展示了如何用顶层 await 去加载带有后备方案的依赖。如果 CDN A 无法导入 jQuery,那么会尝试从 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 的特性,Rich Harris 提出了不少 抨击性的问题 [8] :

  • 顶层 await 会阻塞代码的执行
  • 顶层 await 会阻塞资源的获取
  • CommonJS 模块没有明确的互操作方案

而 stage 3 的提案已经直接解决了这些问题:

  • 由于兄弟模块能够执行,所以不存在阻塞;

  • 顶层 await 在模块图的执行阶段发挥作用,此时所有的资源都已经获取并链接了,因此不存在资源被阻塞的风险;
  • 顶层 await 只限于在 ES6 模块中使用,本身就不打算支持普通脚本或者 CommonJS 模块

我强烈推荐各位读者阅读提案的 FAQ [9] 来加深对这个新特性的理解。

看到这里,想必你对这个酷炫的新特性已经有了一定的了解。是不是已经迫不及待要使用看看了呢?在评论区留言一起交流吧。

参考资料

[1]

Why Should You Use Top-level Await in JavaScript?: https://blog.bitsrc.io/why-should-you-use-top-level-await-in-javascript-a3ba8139ef23

[2]

Mahdhi Rezvi: https://medium.com/@mahdhirezvi?source=post_page-----a3ba8139ef23--------------------------------

[3]

async function : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

[4]

文档问答: https://github.com/tc39/proposal-top-level-await#faq

[5]

文档: https://github.com/tc39/proposal-top-level-await#implementations

[6]

指南: https://medium.com/@pprathameshmore/top-level-await-support-in-node-js-v14-3-0-8af4f4a4d478

[7]

提案: https://github.com/tc39/proposal-top-level-await#use-cases

[8]

抨击性的问题: https://gist.github.com/Rich-Harris/0b6f317657f5167663b493c722647221

[9]

FAQ: https://github.com/tc39/proposal-top-level-await#faq


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK