為何在 JavaScript 中使用頂層 await?

作為一門非常靈活和強(qiáng)大的語(yǔ)言,JavaScript 對(duì)現(xiàn)代 web 產(chǎn)生了深遠(yuǎn)的影響。它之所以能夠在 web 開(kāi)發(fā)中占據(jù)主導(dǎo)地位,其中一個(gè)主要原因就是頻繁更新所帶來(lái)的持續(xù)改進(jìn)。
頂層 await(top-level await)是近年來(lái)提案中涉及的新特性。該特性可以讓 ES 模塊對(duì)外表現(xiàn)為一個(gè) async 函數(shù),允許 ES 模塊去 await 數(shù)據(jù)并阻塞其它導(dǎo)入這些數(shù)據(jù)的模塊。只有在數(shù)據(jù)確定并準(zhǔn)備好的時(shí)候,導(dǎo)入數(shù)據(jù)的模塊才可以執(zhí)行相應(yīng)的代碼。
有關(guān)該特性的提案目前仍處于 stage 3 階段,因此我們不能直接在生產(chǎn)環(huán)境中使用。但鑒于它將在不久的未來(lái)推出,提前了解一下還是大有好處的。
聽(tīng)起來(lái)一頭霧水沒(méi)關(guān)系,繼續(xù)往下閱讀,我會(huì)和你一起搞定這個(gè)新特性的。
以前的寫法,問(wèn)題在哪里?
在引入頂層 await 之前,如果你試圖在一個(gè) async 函數(shù)外面使用 await 關(guān)鍵字,將會(huì)引起語(yǔ)法錯(cuò)誤。為了避免這個(gè)問(wèn)題,開(kāi)發(fā)者通常會(huì)使用立即執(zhí)行函數(shù)表達(dá)式(IIFE)
await Promise.resolve(console.log('??'));
//報(bào)錯(cuò)
(async () => {
await Promise.resolve(console.log('??'));
//??
})();
然而這只是冰山一角
在使用 ES6 模塊化的時(shí)候,經(jīng)常會(huì)遇到需要導(dǎo)入導(dǎo)出的場(chǎng)景??纯聪旅孢@個(gè)例子:
//------ 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};
在這個(gè)例子中,我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">library.js 和middleware.js 之間進(jìn)行變量的導(dǎo)入導(dǎo)出 (文件名隨意,這里不是重點(diǎn))
如果仔細(xì)閱讀,你會(huì)注意到有一個(gè) delay 函數(shù),它返回的 Promise 會(huì)在計(jì)時(shí)結(jié)束之后被 resolve。因?yàn)檫@是一個(gè)異步操作(在真實(shí)的業(yè)務(wù)場(chǎng)景中,這里可能會(huì)是一個(gè) fetch 調(diào)用或者某個(gè)異步任務(wù)),我們?cè)?async IIFE 中使用 await 以等待其執(zhí)行結(jié)果。一旦 promise 被 resolve,我們會(huì)執(zhí)行從 library.js 中導(dǎo)入的函數(shù),并將計(jì)算得到的結(jié)果賦值給兩個(gè)變量。這意味著,在 promise 被 resolve 之前,兩個(gè)變量都會(huì)是 undefined。
在代碼最后面,我們將計(jì)算得到的兩個(gè)變量導(dǎo)出,供另一個(gè)模塊使用。
下面這個(gè)模塊負(fù)責(zé)導(dǎo)入并使用上述兩個(gè)變量:
//------ 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
運(yùn)行上面代碼,你會(huì)發(fā)現(xiàn)前兩次打印得到的都是 undefined,后兩次打印得到的是 169 和 13。為什么會(huì)這樣呢?
這是因?yàn)椋?async 函數(shù)執(zhí)行完畢之前,main.js 就已經(jīng)訪問(wèn)了 middleware.js 導(dǎo)出的變量。記得嗎?我們前面還有一個(gè) promise 等待被 resolve 呢 ……
為了解決這個(gè)問(wèn)題,我們需要想辦法通知模塊,讓它在準(zhǔn)備好訪問(wèn)變量的時(shí)候再將變量導(dǎo)入。
解決方案
針對(duì)上述問(wèn)題,有兩個(gè)廣泛使用的解決方案:
1.導(dǎo)出一個(gè) Promise 表示初始化
你可以導(dǎo)出一個(gè) IIFE 并依靠它確定可以訪問(wèn)導(dǎo)出結(jié)果的時(shí)機(jī)。async 關(guān)鍵字可以異步化一個(gè)方法,并相應(yīng)返回一個(gè) promise[3]。因此,下面的代碼中,async IIFE 會(huì)返回一個(gè) 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('??'));
}, delayInms);
});
}
export {squareOutput,diagonalOutput};
當(dāng)你在 main.js 中訪問(wèn)導(dǎo)出結(jié)果的時(shí)候,你可以靜待 async IIFE 被 resolve,之后再去訪問(wèn)變量。
//------ 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
})
盡管這個(gè)方案可以生效,但它也引入了新的問(wèn)題:
大家都必須將這種模式作為標(biāo)準(zhǔn)去遵循,而且必須要找到并等待合適的 promise; 倘若有另一個(gè)模塊依賴 main.js中的變量squareOutput和diagonalOutput,那么我們就需要再次書(shū)寫類似的 IIFE promise 并導(dǎo)出,從而讓另一個(gè)模塊得以正確地訪問(wèn)變量。
為了解決這兩個(gè)新問(wèn)題,第二個(gè)方案應(yīng)運(yùn)而生。
2.用導(dǎo)出的變量去 resolve IIFE promise
在這個(gè)方案中,我們不再像之前那樣單獨(dú)導(dǎo)出變量,而是將變量作為 async IIFE 的返回值返回。這樣的話,main.js 只需簡(jiǎn)單地等待 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('??'));
}, 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
})
但這個(gè)方案有其自身的復(fù)雜性存在。
根據(jù)提案的說(shuō)法,“這種模式的不良影響在于,它要求對(duì)相關(guān)數(shù)據(jù)進(jìn)行大規(guī)模重構(gòu)以使用動(dòng)態(tài)模式;同時(shí),它將模塊的大部分內(nèi)容放在 .then() 的回調(diào)函數(shù)中,以使用動(dòng)態(tài)導(dǎo)入。從靜態(tài)分析、可測(cè)試性、工程學(xué)以及其它角度來(lái)講,這種做法相比 ES2015 的模塊化來(lái)說(shuō)是一種顯而易見(jiàn)的倒退”。
頂層 Await 是如何解決上述問(wèn)題的?
頂層 await 允許我們讓模塊系統(tǒng)去處理 promise 之間的協(xié)調(diào)關(guān)系,從而讓我們這邊的工作變得異常簡(jiǎn)單。
//------ 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
在 middleware.js 中的 await promise 被 resolve 之前, main.js 中任意一條語(yǔ)句都不會(huì)執(zhí)行。與之前提及的解決方案相比,這個(gè)方法要簡(jiǎn)潔得多。
注意
必須注意的是,頂層 await 只在 ES 模塊中生效。 此外,你必須要顯式聲明模塊之間的依賴關(guān)系,才能讓頂層 await 像預(yù)期那樣生效。提案?jìng)}庫(kù)中的這段代碼就很好地說(shuō)明了這個(gè)問(wèn)題:
// 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
這段代碼打印的順序并不是預(yù)想中的 X1,X2,Y。這是因?yàn)?x 和 y 是獨(dú)立的模塊,互相之間沒(méi)有依賴關(guān)系。
推薦你閱讀一下 文檔問(wèn)答[4] ,這樣會(huì)對(duì)這個(gè)頂層 await 這個(gè)新特性有更加全面的了解。
試用
V8
你可以按照文檔[5]所說(shuō)的,嘗試使用頂層 await 特性。
我使用的是 V8 的方法。找到你電腦上 Chrome 瀏覽器的安裝位置,確保關(guān)閉瀏覽器的所有進(jìn)程,打開(kāi)命令行運(yùn)行如下命令:
chrome.exe --js-flags="--harmony-top-level-await"
這樣,Chrome 重新打開(kāi)后將開(kāi)啟對(duì)于頂層 await 特性的支持。
當(dāng)然,你也可以在 Node 環(huán)境測(cè)試。閱讀這個(gè)指南[6] 獲取更多細(xì)節(jié)。
ES 模塊
確保在 script 標(biāo)簽中聲明該屬性:type="module"
<script type="module" src="./index.js" >
</script>
需要注意的是,和普通腳本不一樣,聲明模塊化之后的腳本會(huì)受到 CORS 策略的影響,因此你需要通過(guò)服務(wù)器打開(kāi)該文件。
應(yīng)用場(chǎng)景
以下是提案[7]中講到的相關(guān)用例:
動(dòng)態(tài)的依賴路徑
const strings = await import(`/i18n/${navigator.language}`);
允許模塊使用運(yùn)行時(shí)的值去計(jì)算得到依賴關(guān)系。這對(duì)生產(chǎn)/開(kāi)發(fā)環(huán)境的區(qū)分以及國(guó)際化工作等非常有效。
資源初始化
const connection = await dbConnector();
這有助于把模塊看作某種資源,同時(shí)可以在模塊不存在的時(shí)候拋出錯(cuò)誤。錯(cuò)誤可以在下面介紹的后備方案中得到處理。
依賴的后備方案
下面的例子展示了如何用頂層 await 去加載帶有后備方案的依賴。如果 CDN A 無(wú)法導(dǎo)入 jQuery,那么會(huì)嘗試從 CDN B 中導(dǎo)入。
let jQuery;
try {
jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.example.com/jQuery');
}
抨擊
針對(duì)頂層 await 的特性,Rich Harris 提出了不少抨擊性的問(wèn)題[8]:
頂層 await會(huì)阻塞代碼的執(zhí)行頂層 await會(huì)阻塞資源的獲取CommonJS 模塊沒(méi)有明確的互操作方案
而 stage 3 的提案已經(jīng)直接解決了這些問(wèn)題:
由于兄弟模塊能夠執(zhí)行,所以不存在阻塞; 頂層 await在模塊圖的執(zhí)行階段發(fā)揮作用,此時(shí)所有的資源都已經(jīng)獲取并鏈接了,因此不存在資源被阻塞的風(fēng)險(xiǎn);頂層 await只限于在 ES6 模塊中使用,本身就不打算支持普通腳本或者 CommonJS 模塊
我強(qiáng)烈推薦各位讀者閱讀提案的 FAQ[9] 來(lái)加深對(duì)這個(gè)新特性的理解。
看到這里,想必你對(duì)這個(gè)酷炫的新特性已經(jīng)有了一定的了解。是不是已經(jīng)迫不及待要使用看看了呢?在評(píng)論區(qū)留言一起交流吧。
參考資料[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]文檔問(wèn)答: 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]抨擊性的問(wèn)題: https://gist.github.com/Rich-Harris/0b6f317657f5167663b493c722647221
[9]FAQ: https://github.com/tc39/proposal-top-level-await#faq
1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)
程序員成長(zhǎng)指北,回復(fù)「1」加入高級(jí)前端交流群!「在這里有好多 前端 開(kāi)發(fā)者,會(huì)討論 前端 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長(zhǎng)。
“在看轉(zhuǎn)發(fā)”是最大的支持
