async/await ES6 Promise 的最佳實(shí)踐(經(jīng)驗(yàn)分享)
譯文來(lái)自 https://dev.to/somedood/best-practices-for-es6-promises-36da
作者?Basti Ortiz (Some Dood)
ES6 promise 是非常棒的一個(gè)功能, 它是 JavaScript 異步編程中不可或缺的部分,并且取代了以 回調(diào)地獄而聞名的基于回調(diào)的模式。
然而 promises 的概念并不是非常容易理解。在本文中,我將討論這些年來(lái)學(xué)到的最佳實(shí)踐,這些最佳實(shí)踐可以幫助我充分利用異步 JavaScript。
處理 promise rejections
沒(méi)有什么比 unhandled promise rejection(未處理的 promise 錯(cuò)誤) 更讓人頭疼了。當(dāng)一個(gè) promise 拋出一個(gè)錯(cuò)誤,但你沒(méi)有使用Promise#catch來(lái)捕獲程序錯(cuò)誤時(shí),就出現(xiàn)這種情況。
在調(diào)試高并發(fā)的應(yīng)用程序時(shí),由于錯(cuò)誤信息晦澀難懂(令人頭疼),所以想要找到出錯(cuò)的 promise 是非常困難的。然而,一旦找到出錯(cuò)的 promise 并被認(rèn)為是可復(fù)現(xiàn)的,但是應(yīng)用程序本身的并發(fā)性,應(yīng)用程序的狀態(tài)通常也同樣難以確定。總的來(lái)說(shuō),這非常的糟糕。
解決方案很簡(jiǎn)單:雖然你認(rèn)為程序不會(huì)出錯(cuò),但還是要為可能出錯(cuò)的 promises 附加一個(gè) Promise#catch 處理程序。
此外,在未來(lái)的 Node.js 版本中,未處理的 promise reject 將使 Node 進(jìn)程崩潰。良好的習(xí)慣能夠有效降低出錯(cuò)的概率,現(xiàn)在就是養(yǎng)成良好習(xí)慣的時(shí)機(jī)。
保持它的"線性"
https://dev.to/somedood/please-don-t-nest-promises-3o1o
在之前的一篇文章中,我解釋了避免嵌套 promises 的重要性。簡(jiǎn)而言之,嵌套 promise 又回到了 "回調(diào)地獄 "的模式。promises 的目的是為異步編程提供符合習(xí)慣的標(biāo)準(zhǔn)化語(yǔ)義。如果嵌套 promises,我們又回到了 Node.js api 中流行的冗長(zhǎng)而又相當(dāng)麻煩的錯(cuò)誤優(yōu)先回調(diào)(https://nodejs.org/api/errors.html#errors_error_first_callbacks)。
Node.js 核心 API 公開的大多數(shù)異步方法都遵循慣用模式,稱為錯(cuò)誤優(yōu)先回調(diào)。通過(guò)這種模式,回調(diào)函數(shù)作為參數(shù)傳遞給方法。當(dāng)操作完成或引發(fā)錯(cuò)誤時(shí),將以 Error 對(duì)象(如果有)作為第一個(gè)參數(shù)傳遞來(lái)調(diào)用回調(diào)函數(shù)。如果未引發(fā)錯(cuò)誤,則第一個(gè)參數(shù)將作為 null 傳遞。
為了保持異步活動(dòng)的“線性”,我們可以使用async 函數(shù)[1]或線性的鏈?zhǔn)?promises。
import?{?promises?as?fs?}?from?"fs";
//?嵌套?Promises
fs.readFile("file.txt").then((text1)?=>
??fs.readFile(text1).then((text2)?=>?fs.readFile(text2).then(console.log))
);
//?線性鏈?zhǔn)?Promises
const?readOptions?=?{?encoding:?"utf8"?};
const?readNextFile?=?(fname)?=>?fs.readFile(fname,?readOptions);
fs.readFile("file.txt",?readOptions)
??.then(readNextFile)
??.then(readNextFile)
??.then(console.log);
//?async?函數(shù)
async?function?readChainOfFiles()?{
??const?file1?=?await?readNextFile("file.txt");
??const?file2?=?await?readNextFile(file1);
??console.log(file2);
}
util.promisify 是你最好的伙伴
當(dāng)我們從錯(cuò)誤優(yōu)先回調(diào)過(guò)渡到 ES6 promises 時(shí),我們習(xí)慣于養(yǎng)成一切 promisifying 化。
在大多數(shù)情況下,用 Promise 構(gòu)造函數(shù)包裝基于回調(diào)的舊 API 就足夠了。一個(gè)典型的例子是將 `globalThis.setTimeout`[2] 作為sleep函數(shù)
const?sleep?=?ms?=>?new?Promise(
??resolve?=>?setTimeout(resolve,?ms)
);
await?sleep(1000);
但是,其他的外部庫(kù)未必會(huì) "友好 " 地使用的 promises。如果我們不小心,可能會(huì)出現(xiàn)某些不可預(yù)見的副作用--比如內(nèi)存泄漏。在 Node.js 環(huán)境中,util.promisify 函數(shù)的存在就是為了解決這個(gè)問(wèn)題。
顧名思義,util.promisify可以做兼容和簡(jiǎn)化基于回調(diào)的 API 的包裝。它假定給定函數(shù)像大多數(shù) Node.js API 一樣接受錯(cuò)誤優(yōu)先的回調(diào)作為其最終參數(shù)。如果存在特殊的實(shí)現(xiàn)細(xì)節(jié)[3],則庫(kù)作者還可以提供 自定義 promisifier[4]。
import?{?promisify?}?from?"util";
const?sleep?=?promisify(setTimeout);
await?sleep(1000);
避免順序陷阱
https://dev.to/somedood/javascript-concurrency-avoiding-the-sequential-trap-7f0
在本系列的上一篇文章中,我大量討論了調(diào)度多個(gè)獨(dú)立的 Promise 的功能。由于 promise 的順序性,promise 鏈只能使我們走到目前為止。(換句話說(shuō),promise 鏈?zhǔn)街械娜蝿?wù)是按順序執(zhí)行的,譯者注) 因此,讓程序的 "idle time(空閑時(shí)間)" 最小化的關(guān)鍵是并發(fā)。(以下使用 Promise.all 來(lái)實(shí)現(xiàn)并發(fā),譯者注)
import?{?promisify?}?from?'util';
const?sleep?=?promisify(setTimeout);
//?Sequential?Code?(~3.0s)
sleep(1000)
??.then(()?=>?sleep(1000));
??.then(()?=>?sleep(1000));
//?Concurrent?Code?(~1.0s)
Promise.all([?sleep(1000),?sleep(1000),?sleep(1000)?]);
注意:promise 也會(huì)阻止事件循環(huán)
關(guān)于 promise 的最大的誤解可能是一種主觀意識(shí),即 "promises 允許執(zhí)行多線程 的 JavaScript"。盡管事件循環(huán)給出了 并行性(parallelism)的錯(cuò)覺,但這僅是錯(cuò)覺。在底層,JavaScript 仍然是單線程的。
事件循環(huán)只允許運(yùn)行時(shí)并發(fā)地進(jìn)行調(diào)度、編排和處理事件。不嚴(yán)格地講,這些“事件”確實(shí)是并行發(fā)生的,但是當(dāng)時(shí)間到了,它們?nèi)詫错樞蛱幚怼?/p>
在下面的示例中,promise 不會(huì)使用給定的執(zhí)行程序函數(shù)生成新線程。實(shí)際上,執(zhí)行函數(shù)總是在構(gòu)造 promise 時(shí)立即執(zhí)行,從而阻塞事件循環(huán)。執(zhí)行程序函數(shù)返回后,將恢復(fù)頂層執(zhí)行。resolve 的返回值 (Promise#then處理程序的代碼)被延遲到當(dāng)前調(diào)用堆棧完成剩余的頂級(jí)代碼。
由于 Promise 不會(huì)自動(dòng)產(chǎn)生新線程,因此在后續(xù)Promise#then處理程序中占用大量 CPU 的工作也會(huì)阻塞事件循環(huán)。
Promise.resolve()
??//.then(...)
??//.then(...)
??.then(()?=>?{
????for?(let?i?=?0;?i?1e9;?++i)?continue;
??});
考慮內(nèi)存使用情況
由于某些不必需的堆分配[5],promises 往往會(huì)占用相對(duì)較高的內(nèi)存和計(jì)算成本。
除了存儲(chǔ)有關(guān) Promise 實(shí)例本身的信息(例如其屬性和方法)之外,JavaScript 運(yùn)行時(shí)還動(dòng)態(tài)分配更多內(nèi)存以跟蹤與每個(gè) Promise 相關(guān)的異步活動(dòng)。
此外,考慮到 Promise API 大量使用了閉包和回調(diào)函數(shù)(它們都需要自己的堆分配),令人驚訝的是,一個(gè) promise 就需要大量的內(nèi)存。大量的 promises 可能被證明在熱代碼路徑(hot-code-path )(https://english.stackexchange.com/questions/402436/whats-the-meaning-of-hot-codepath-or-hot-code-path)中。(在熱代碼路徑中分配堆,可能會(huì)觸發(fā)垃圾收集,會(huì)導(dǎo)致性能的極端惡化,因此能少用就好用,譯者注,相關(guān)信息 https://stackoverflow.com/questions/22894877/avoid-allocations-in-compiler-hot-paths-roslyn-coding-conventions)。
通常來(lái)講,Promise 的每個(gè)新實(shí)例都需要大量堆分配來(lái)存儲(chǔ)屬性,方法,閉包和異步狀態(tài)。我們使用的 promise 越少,從長(zhǎng)遠(yuǎn)來(lái)看,性能會(huì)越好。
同步的 promise 是不必要且多余的
像前面所說(shuō),promise 不會(huì)神奇地產(chǎn)生新線程。因此,一個(gè)完全同步的執(zhí)行器函數(shù)(對(duì)于 Promise 構(gòu)造函數(shù))僅僅是一個(gè)不必要的中間層。
const?promise1?=?new?Promise((resolve)?=>?{
??//?Do?some?synchronous?stuff?here...
??resolve("Presto");
});
類似地,將Promise#then處理程序附加到同步解析的 Promise 只會(huì)稍微延遲代碼的執(zhí)行。對(duì)于此用例,最好使用 global.setImmediate。
promise1.then((name)?=>?{
??//?This?handler?has?been?deferred.?If?this
??//?is?intentional,?one?would?be?better?off
??//?using?`setImmediate`.
});
舉例來(lái)說(shuō),如果執(zhí)行程序函數(shù)不包含異步 I/O 操作,則它僅充當(dāng)不必要的中間層,承擔(dān)不必要的內(nèi)存和計(jì)算開銷。
因此,我個(gè)人不鼓勵(lì)自己在項(xiàng)目中使用Promise.resolve和Promise.reject。這些靜態(tài)方法的主要目的是在 promise 中優(yōu)化包裝一個(gè)值。所產(chǎn)生的 promise 將立即得到 resolve,因此可以說(shuō)一開始就不需要 promise(除非出于 API 兼容性的考慮)。
//?Chain?of?Immediately?Settled?Promises
const?resolveSync?=?Promise.resolve.bind(Promise);
Promise.resolve("Presto")
??.then(resolveSync)?//?Each?invocation?of?`resolveSync`?(which?is?an?alias
??.then(resolveSync)?//?for?`Promise.resolve`)?constructs?a?new?promise
??.then(resolveSync);?//?in?addition?to?that?returned?by?`Promise#then`.
長(zhǎng)的 promise 鏈應(yīng)該引起一些注意
有時(shí)需要串行執(zhí)行多個(gè)異步操作。在這種情況下,promise 鏈?zhǔn)抢硐搿?/p>
但是,必須注意,由于 Promise API 是可以鏈?zhǔn)秸{(diào)用的,因此每次調(diào)用Promise#then都會(huì)構(gòu)造并返回一個(gè)新的 Promise 實(shí)例(保留了某些先前的狀態(tài))。考慮到中間處理程序會(huì)創(chuàng)建其他 promise,長(zhǎng)鏈有可能對(duì)內(nèi)存和 CPU 使用率造成重大損失。
const?p1?=?Promise.resolve("Presto");
const?p2?=?p1.then((x)?=>?x);
//?The?two?`Promise`?instances?are?different.
p1?===?p2;?//?false
換句話說(shuō),所有中間處理程序必須嚴(yán)格地是異步的,也就是說(shuō),它們返回 promises。只有最終處理程序保留運(yùn)行同步代碼的權(quán)利。(最后一個(gè) .then 才配擁有全部同步代碼執(zhí)行的權(quán)利,這樣的方式能夠提高性能,譯者注)
import?{?promises?as?fs?}?from?"fs";
//?This?is?**not**?an?optimal?chain?of?promises
//?based?on?the?criteria?above.
const?readOptions?=?{?encoding:?"utf8"?};
fs.readFile("file.txt",?readOptions)
??.then((text)?=>?{
????//?Intermediate?handlers?must?return?promises.
????const?filename?=?`${text}.docx`;
????return?fs.readFile(filename,?readOptions);
??})
??.then((contents)?=>?{
????//?This?handler?is?fully?synchronous.?It?does?not
????//?schedule?any?asynchronous?operations.?It?simply
????//?processes?the?result?of?the?preceding?promise
????//?only?to?be?wrapped?(as?a?new?promise)?and?later
????//?unwrapped?(by?the?succeeding?handler).
????const?parsedInteger?=?parseInt(contents);
????return?parsedInteger;
??})
??.then((parsed)?=>?{
????//?Do?some?synchronous?tasks?with?the?parsed?contents...
??});
如上面的示例所示,完全同步的中間處理程序帶來(lái)了對(duì) Promise 的冗余包裝和 resolve 值。這就是為什么我們要遵循最佳 peomise 鏈的策略。為了消除冗余,我們可以簡(jiǎn)單地將有問(wèn)題的中間處理程序的工作集成到后續(xù)處理程序中。
import?{?promises?as?fs?}?from?"fs";
const?readOptions?=?{?encoding:?"utf8"?};
fs.readFile("file.txt",?readOptions)
??.then((text)?=>?{
????//?Intermediate?handlers?must?return?promises.
????const?filename?=?`${text}.docx`;
????return?fs.readFile(filename,?readOptions);
??})
??.then((contents)?=>?{
????//?This?no?longer?requires?the?intermediate?handler.
????const?parsed?=?parseInt(contents);
????//?Do?some?synchronous?tasks?with?the?parsed?contents...
??});
(簡(jiǎn)而言之,promise 鏈能短則短,避免不必要的開銷,譯者注。)
保持簡(jiǎn)單
如果不需要它們,請(qǐng)不要使用它們。就這么簡(jiǎn)單。
創(chuàng)建 Promises 的代價(jià)并不是"免費(fèi)"的。它們本身不觸發(fā) JavaScript 中的 "并行性"。(也就是不會(huì)讓代碼執(zhí)行更快,譯者注) 它們只是用于調(diào)度和處理異步操作的標(biāo)準(zhǔn)化抽象。如果我們編寫的代碼不是異步的,那么就不需要 promises。
然后,通常情況下,我們確實(shí)需要在應(yīng)用程序中使用 promises。這就是為什么我們必須了解所有最佳實(shí)踐,取舍,陷阱和誤區(qū)。當(dāng)然所有的一切,僅僅是最小量使用的問(wèn)題 – 不是因?yàn)?promise 是"惡魔",而是提醒大家不要濫用他們。
故事未完待續(xù)。在本系列的下一部分中,我將把最佳實(shí)踐的討論擴(kuò)展到 ES2017 異步函數(shù)[6]((`async`/`await`)[7].)
參考資料
async 函數(shù): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
[2]globalThis.setTimeout: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
實(shí)現(xiàn)細(xì)節(jié): https://dev.to/somedood/best-practices-for-es6-promises-36da#fn1
[4]自定義 promisifier: https://nodejs.org/api/util.html#util_custom_promisified_functions
[5]堆分配: https://www.youtube.com/watch?v=wJ1L2nSIV1s
[6]ES2017 異步函數(shù): https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
[7](async/await): https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。

