前言
原文地址:https://2ality.com/2020/09/ecmascript-2021.html
作者:Dr. Axel Rauschmayer
「ECMAScript」提案 Top-level await
由 Myles Borins 提出,它能够让你在模块的最高层中应用 await
操作符。在这之前,你只能通过在 async
函数或 async generators
中应用 await
操作符。
1 为什么要在模块的最高层级应用 await
为什么咱们须要在模块的最高层级中应用 await
操作符?因为,这能够让咱们初始化一个须要 异步加载数据 的模块。接下来的三个大节,将会向你展现在什么场景下 Top-level await
会十分实用。
1.1 动静加载模块
const params = new URLSearchParams(window.locaion.search);
const language = params.get('lang');
const messasges = await import(`./messages-${language}.mjs`); // {A}
console.log(messages.welcome);
在 A 行,咱们动静地引入模块。得益于 Top-level await
,这让咱们应用起来和一般、动态地引入模块一样便捷。
1.2 如果模块加载失败调用对应回调
let lodash;
try {lodash = await import('https://primary.example.com/lodash');
} catch {lodash = await import('https://secondary.example.com/lodash');
}
1.3 应用最快加载好的资源
const resource = await Promise.any([fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
]);
应用了 Promise.any()
,变量 resource
会初始化为最快加载好的那个资源,
2 为什么应用变通方法实现 Top-level await
不好
在本节中,咱们尝试实现一个模块,该模块通过异步加载数据的模式初始化其导出。
咱们先尝试应用变通方法实现 Top-level await
。而,这些办法都会有毛病。因而,咱们最终的 Top-level await
将是最佳的解决方案。
2.1 第一个尝试:立刻执行的 top-level async 函数
上面这个模块会异步初始化 downloadedText1
并将其导出:
// async-lib1.mjs
export let downloadedText1;
async function main() {downloadedText1 = await asyncFunction()
}
main()
这里,咱们不是申明和调用 async
函数,能够应用立刻执行的箭头函数:
export let downloadedText;
(async () => {downloadedText = await asyncFunction();
})();
须要留神的是,咱们必须始终将箭头函数包裹在括号中:
- 调用的括号不能放在箭头函数主体外。
- 即便在表达式上下文中,咱们也不能去掉箭头函数四周的括号。
为了理解这种办法的毛病,咱们来应用一下 async-lib.mjs
:
import {downloadedText1} from './async-lib1.mjs';
assert.equal(downloadedText1, undefined); // (A)
setTimeout(() => {assert.equal(downloadedText1, 'Downloaded!'); // (B)
}, 100);
在正确地引入 async-lib.mjs
后,downloadedText1
会是 undefined
(A 行)。在咱们能够失常拜访 downloaderText1
之前,必须期待异步函数执行结束(B 行)。
咱们须要找到一种牢靠的办法来实现,目前的办法并不稳当。例如,如果异步函数执行破费超过 100 毫秒,setTimeout
将不起作用。
2.2 第二个尝试:当导出模块能够失常应用时告知引入的程序
引入的程序须要晓得什么时候是能够失常拜访异步函数初始化并导出的模块。咱们能够通过 Promise
已实现来让它们晓得:
// async-lib2.mhs
export let downloadedText2;
export const done = (async () => {downloadedText2 = await asyncFunction();
})();
这个立刻执行的异步箭头函数会同步地返回一个已实现(fulfilled)的值为 undefined
的 Promise
。它的实现是隐式的,因为咱们不返回任何货色。
引入的程序当初期待实现,就能够失常地拜访 downloadedText2
:
// main2.mjs
import {done, downloadedText2} from './async-lib2.mjs';
export default done.then(() => {assert.equal(downloadedText2, 'Downloaded!');
});
这个办法存在几个毛病:
- 引入的程序必须理解这种模式并且正确地应用。
- 引入的程序很容易了解错这个模式,因为,在
done
完结前,downloadedText2
曾经能够被拜访。 - 这种模式是有问题的:如果
main2.mjs
也应用了这种模式并且导出本人的Promise
,则其只能被其余模块导入。
在咱们接下来的尝试中,咱们将会修复第二点。
2.3 第三个尝试:将导出模块放到一个通过 Promise 传递的对象
在导出模块初始化之前,咱们想导入的程序是不能拜访它的。咱们通过 default-exporting
的模式导出一个已实现的(fulfilled)蕴含咱们导出模块对象的 Promise
:
// async-lib3.mjs
export default (async () => {const downloadedText = await asyncFunction();
return {downloadedText};
})();
async-lib3.mjs
的用法如下:
import asyncLib3 from './async-lib3.mjs';
asyncLib3.then(({downloadedText}) => {assert.equal(downloadedText, 'Downloaded!');
});
这个新的实现形式是最好的,然而咱们的导出不再是动态的,它们是动静地创立。因而,咱们失去了动态构造的所有益处(好的工具反对、更好的性能等等。)。
尽管,这种模式能够更容易地被正确应用,然而依然存在问题。
2.4 最终的尝试:Top-level await
Top-level await
在保留长处的同时,打消了咱们以上办法的所有毛病:
// async-lib4.mjs
export const downloadedText4 = await asyncFunction();
咱们依然异步地初始化咱们的导出,然而咱们能够通过 Top-level await
来失常地应用 downloadedText4
。
咱们能够导入 async-lib4.mjs
,而不须要晓得它会异步初始化的导出:
import {downloadedText4} from './async-lib4.mjs';
assert.equal(downloadedText4, 'Downloaded!');
那么,下一节,咱们将解释「JavaScript」是如何在幕后确保一切正常地运行。
3 Top-level await 在幕后是如何运行的
思考以下两个文件:
// first.mjs
const response = await fetch('http://example.com/first.txt');
export const first = await response.text();
// main.mjs
import {first} from './first.mjs';
import {second} from './second.mjs';
assert.equal(first, 'First!');
assert.equal(second, 'Second!');
这两者大抵等于以下代码:
// first.mjs
export let first;
export const promise = (async () => {const response = await fetch('http://example.com/first.txt');
first = await response.text();})();
// main.mjs
import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => {await Promise.all([firstPromise, secondPromise]);
assert.equal(first, 'First content!');
assert.equal(second, 'Second content!');
})();
「JavaScript」会动态地确认哪些模块是异步的(即间接导入或间接导入都会有一个 Top-level await
)。这些模块导出的 Promise
都会放到 Promise.all()
中。其余的导入依然照常解决。
须要留神的是,回绝(reject)和同步的异样都会被转为异步函数。
4 Top-level await 的利与弊
利是尽管大家能够通过各种模式来导入异步初始化模块(例如咱们在文章中看到的那些),然而 Top-level await
更易于应用,并使得异步初始化对导入程序变得通明。
弊是 Top-level await
提早了导入模块的初始化。因而,最好审慎应用。对于须要破费很长时间的异步工作能够放到前面或者按需引入。
然而,即便没有应用 Top-level await
也会阻塞导入(例如,如果顶层的有限循环),因而,阻塞并不是拥护应用它的理由。
往期文章回顾
深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?
深度解读 Vue 3 源码 | compile 和 runtime 联合的 patch 过程
深度解读 Vue 3 源码 | 从编译过程,了解动态节点晋升
❤️爱心三连击
通过浏览,如果你感觉有播种的话,能够爱心三连击!!!