【JS】874- 為何在 JavaScript 中使用頂層 await?

作為一門非常靈活和強大的語言,JavaScript 對現(xiàn)代 web 產(chǎn)生了深遠的影響。它之所以能夠在 web 開發(fā)中占據(jù)主導地位,其中一個主要原因就是頻繁更新所帶來的持續(xù)改進。
頂層 await(top-level await)是近年來提案中涉及的新特性。該特性可以讓 ES 模塊對外表現(xiàn)為一個 async 函數(shù),允許 ES 模塊去 await 數(shù)據(jù)并阻塞其它導入這些數(shù)據(jù)的模塊。只有在數(shù)據(jù)確定并準備好的時候,導入數(shù)據(jù)的模塊才可以執(zhí)行相應(yīng)的代碼。
有關(guān)該特性的提案目前仍處于 stage 3 階段,因此我們不能直接在生產(chǎn)環(huán)境中使用。但鑒于它將在不久的未來推出,提前了解一下還是大有好處的。
聽起來一頭霧水沒關(guān)系,繼續(xù)往下閱讀,我會和你一起搞定這個新特性的。
以前的寫法,問題在哪里?
在引入頂層 await 之前,如果你試圖在一個 async 函數(shù)外面使用 await 關(guān)鍵字,將會引起語法錯誤。為了避免這個問題,開發(fā)者通常會使用立即執(zhí)行函數(shù)表達式(IIFE)
await?Promise.resolve(console.log('??'));
//報錯
(async?()?=>?{
?await?Promise.resolve(console.log('??'));
??//??
})();
然而這只是冰山一角
在使用 ES6 模塊化的時候,經(jīng)常會遇到需要導入導出的場景??纯聪旅孢@個例子:
//------?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};
在這個例子中,我們在library.js 和middleware.js 之間進行變量的導入導出 (文件名隨意,這里不是重點)
如果仔細閱讀,你會注意到有一個 delay 函數(shù),它返回的 Promise 會在計時結(jié)束之后被 resolve。因為這是一個異步操作(在真實的業(yè)務(wù)場景中,這里可能會是一個 fetch 調(diào)用或者某個異步任務(wù)),我們在 async IIFE 中使用 await 以等待其執(zhí)行結(jié)果。一旦 promise 被 resolve,我們會執(zhí)行從 library.js 中導入的函數(shù),并將計算得到的結(jié)果賦值給兩個變量。這意味著,在 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
運行上面代碼,你會發(fā)現(xiàn)前兩次打印得到的都是 undefined,后兩次打印得到的是 169 和 13。為什么會這樣呢?
這是因為,在 async 函數(shù)執(zhí)行完畢之前,main.js 就已經(jīng)訪問了 middleware.js 導出的變量。記得嗎?我們前面還有一個 promise 等待被 resolve 呢 ……
為了解決這個問題,我們需要想辦法通知模塊,讓它在準備好訪問變量的時候再將變量導入。
解決方案
針對上述問題,有兩個廣泛使用的解決方案:
1.導出一個 Promise 表示初始化
你可以導出一個 IIFE 并依靠它確定可以訪問導出結(jié)果的時機。async 關(guān)鍵字可以異步化一個方法,并相應(yīng)返回一個 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('??'));
????},?delayInms);
??});
}
export?{squareOutput,diagonalOutput};
當你在 main.js 中訪問導出結(jié)果的時候,你可以靜待 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,那么我們就需要再次書寫類似的 IIFE promise 并導出,從而讓另一個模塊得以正確地訪問變量。
為了解決這兩個新問題,第二個方案應(yīng)運而生。
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('??'));
????},?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
})
但這個方案有其自身的復(fù)雜性存在。
根據(jù)提案的說法,“這種模式的不良影響在于,它要求對相關(guān)數(shù)據(jù)進行大規(guī)模重構(gòu)以使用動態(tài)模式;同時,它將模塊的大部分內(nèi)容放在 .then() 的回調(diào)函數(shù)中,以使用動態(tài)導入。從靜態(tài)分析、可測試性、工程學以及其它角度來講,這種做法相比 ES2015 的模塊化來說是一種顯而易見的倒退”。
頂層 Await 是如何解決上述問題的?
頂層 await 允許我們讓模塊系統(tǒng)去處理 promise 之間的協(xié)調(diào)關(guā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 中任意一條語句都不會執(zhí)行。與之前提及的解決方案相比,這個方法要簡潔得多。
注意
必須注意的是,頂層 await 只在 ES 模塊中生效。 此外,你必須要顯式聲明模塊之間的依賴關(guān)系,才能讓頂層 await 像預(yù)期那樣生效。提案倉庫中的這段代碼就很好地說明了這個問題:
//?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。這是因為 x 和 y 是獨立的模塊,互相之間沒有依賴關(guān)系。
推薦你閱讀一下 文檔問答[4] ,這樣會對這個頂層 await 這個新特性有更加全面的了解。
試用
V8
你可以按照文檔[5]所說的,嘗試使用頂層 await 特性。
我使用的是 V8 的方法。找到你電腦上 Chrome 瀏覽器的安裝位置,確保關(guān)閉瀏覽器的所有進程,打開命令行運行如下命令:
chrome.exe --js-flags="--harmony-top-level-await"
這樣,Chrome 重新打開后將開啟對于頂層 await 特性的支持。
當然,你也可以在 Node 環(huán)境測試。閱讀這個指南[6] 獲取更多細節(jié)。
ES 模塊
確保在 script 標簽中聲明該屬性:type="module"
<script?type="module"?src="./index.js"?>
script>
需要注意的是,和普通腳本不一樣,聲明模塊化之后的腳本會受到 CORS 策略的影響,因此你需要通過服務(wù)器打開該文件。
應(yīng)用場景
以下是提案[7]中講到的相關(guān)用例:
動態(tài)的依賴路徑
const?strings?=?await?import(`/i18n/${navigator.language}`);
允許模塊使用運行時的值去計算得到依賴關(guān)系。這對生產(chǎn)/開發(fā)環(huán)境的區(qū)分以及國際化工作等非常有效。
資源初始化
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會阻塞代碼的執(zhí)行頂層 await會阻塞資源的獲取CommonJS 模塊沒有明確的互操作方案
而 stage 3 的提案已經(jīng)直接解決了這些問題:
由于兄弟模塊能夠執(zhí)行,所以不存在阻塞; 頂層 await在模塊圖的執(zhí)行階段發(fā)揮作用,此時所有的資源都已經(jīng)獲取并鏈接了,因此不存在資源被阻塞的風險;頂層 await只限于在 ES6 模塊中使用,本身就不打算支持普通腳本或者 CommonJS 模塊
我強烈推薦各位讀者閱讀提案的 FAQ[9] 來加深對這個新特性的理解。
看到這里,想必你對這個酷炫的新特性已經(jīng)有了一定的了解。是不是已經(jī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]文檔問答:?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

回復(fù)“加群”與大佬們一起交流學習~
點擊“閱讀原文”查看 100+ 篇原創(chuàng)文章
