文章轉(zhuǎn)載自公眾號(hào):印記中文
原文鏈接:dev.to/mpodlasin/3-most-common-mistakes-when-using-promises-in-javascript-oab
譯者:Shopee 金融前端團(tuán)隊(duì) 張鐵山
本文對(duì)開發(fā)者編寫 Promise
時(shí)常出現(xiàn)的幾種錯(cuò)誤進(jìn)行了總結(jié),剖析的一針見血,來看看是不是你平時(shí)所寫?
時(shí)至今日,即使有 async / await 的引入,JavaScript 中 Promises
的編寫規(guī)則對(duì)于所有的 JS 開發(fā)者來說仍然是必不可少的知識(shí)。
JavaScript 在處理異步問題上和其它編程語言不同。因此,即使具有豐富經(jīng)驗(yàn)的開發(fā)人員有時(shí)也會(huì)陷入誤區(qū)。我親身看到過優(yōu)秀的 Python 或 Java 程序員在為 Node.js 或?yàn)g覽器編碼時(shí)犯了非常愚蠢的錯(cuò)誤。
為了避免這些錯(cuò)誤,JavaScript 中的 Promises
有許多編寫細(xì)節(jié)需要考慮。其中有一些純粹是語言風(fēng)格問題,但也有許多是實(shí)際引入、難以跟蹤的錯(cuò)誤。因此,我決定編寫一個(gè)清單,列出開發(fā)人員在使用 Promises
編程時(shí)遇到的三個(gè)最常見的錯(cuò)誤。
將所有內(nèi)容包裝在 Promise 構(gòu)造函數(shù)中
第一個(gè)錯(cuò)誤也是最為明顯的錯(cuò)誤之一,但是我發(fā)現(xiàn)開發(fā)者犯這個(gè)錯(cuò)誤的頻率出奇的高。
當(dāng)?shù)谝淮螌W(xué)習(xí) Promises 時(shí),你會(huì)了解到 Promise 的構(gòu)造函數(shù),這個(gè)構(gòu)造函數(shù)可以用來創(chuàng)建一個(gè)新的 Promises 對(duì)象。
也許因?yàn)槿藗兺ǔJ峭ㄟ^將一些瀏覽器 API(例如 setTimeout
)包裝在 Promise 構(gòu)造函數(shù)中這種方式來開始學(xué)習(xí)的,所以在他們心中根深蒂固地認(rèn)為創(chuàng)建 Promise 對(duì)象的唯一方法是使用構(gòu)造函數(shù)。
因此,通常會(huì)這樣寫:
const createdPromise = new Promise(resolve => {
somePreviousPromise.then(result => {
// 對(duì) result 進(jìn)行一些操作
resolve(result);
});
});
可以看到,為了對(duì) somePreviousPromise
的結(jié)果 result
進(jìn)行一些操作,有些人使用了 then
,但是后來決定將其再次包裝在一個(gè) Promise 的構(gòu)造函數(shù)中,為的是將該操作的結(jié)果存儲(chǔ)在 createdPromise 的變量中,大概是為了稍后對(duì)該 Promise 進(jìn)行更多操作。
這顯然是沒有必要的。then
方法的全部要點(diǎn)在于它本身會(huì)返回一個(gè) Promise,它表示的是執(zhí)行 somePreviousPromise
后再執(zhí)行then
中的的回調(diào)函數(shù),then
的參數(shù)是 somePreviousPromise
成功執(zhí)行返回的結(jié)果。
所以,上一段代碼大致等價(jià)于:
const createPromise = somePreviousPromise.then(result => {
// 對(duì) result 進(jìn)行一些操作
return result
})
如此編寫,會(huì)簡(jiǎn)潔很多。
但是,為什么我說它只是大致等價(jià)呢?區(qū)別在哪里?
經(jīng)驗(yàn)不足且不細(xì)心觀察的話可能很難發(fā)現(xiàn),實(shí)際上兩者在錯(cuò)誤處理上存在巨大的差異,這種差異比第一段代碼的冗余問題更為重要。
假設(shè) somePreviousPromise
出于某些原因失敗了且拋出錯(cuò)誤。例如,這個(gè) Promise 里發(fā)送了一個(gè) HTTP 請(qǐng)求,而 API 響應(yīng) 500 錯(cuò)誤。
事實(shí)證明,在上一段代碼中,我們將一個(gè) Promise 包裝到另一個(gè) Promise 中,我們根本無法捕獲該錯(cuò)誤。為了解決此問題,我們必須進(jìn)行以下更改:
const createdPromise = new Promise((resolve, reject) => {
somePreviousPromise.then(result => {
// 對(duì) result 進(jìn)行一些操作
resolve(result);
}, reject);
});
我們簡(jiǎn)單的在回調(diào)函數(shù)中添加了一個(gè) reject
參數(shù),然后通過將其作為第二個(gè)參數(shù)傳遞給 then
的方式來使用它。請(qǐng)務(wù)必記住,then
方法接受第二個(gè)可選參數(shù)來進(jìn)行錯(cuò)誤處理,這一點(diǎn)非常重要。
現(xiàn)在如果 somePreviousPromise
出于某些原因失敗了,reject
函數(shù)將會(huì)被調(diào)用,并且我們將能夠一如往常地處理 createdPromise
上的錯(cuò)誤。
這樣是否解決了所有問題?抱歉,并沒有。
我們處理了 somePreviousPromise
自身可能發(fā)生的錯(cuò)誤,但是我們?nèi)匀粺o法控制作為 then
方法第一個(gè)參數(shù)的回調(diào)函數(shù)中發(fā)生的情況。在注釋區(qū)域 // 對(duì) result 進(jìn)行一些操作
執(zhí)行的代碼可能會(huì)有一些錯(cuò)誤,如果這塊地方的代碼拋出任何錯(cuò)誤,那 then
方法的第二個(gè)參數(shù)reject
依舊捕獲不到這些錯(cuò)誤。
這是因?yàn)樽鳛?then
方法的第二個(gè)參數(shù)的錯(cuò)誤處理函數(shù)只對(duì) Promise 鏈上當(dāng)前 then
之前發(fā)生的錯(cuò)誤作出響應(yīng)。
因此,最合適的(也是最終的)解決方案應(yīng)該如下:
const createdPromise = new Promise((resolve, reject) => {
somePreviousPromise.then(result => {
// 對(duì) result 進(jìn)行一些操作
resolve(result);
}).catch(reject);
});
注意,這次我們使用了 catch
方法 —— 因?yàn)樗鼘⒃诘谝粋€(gè) then
之后被調(diào)用,它將捕獲到 Promise 鏈上拋出的所有錯(cuò)誤。無論是 somePreviousPromise
還是 then
中的回調(diào)失敗了,Promise
都將按預(yù)期處理這些情況。
從上述示例可以發(fā)現(xiàn),在 Promise 的構(gòu)造函數(shù)中包裝代碼時(shí),有很多細(xì)節(jié)問題需要處理。這就是為什么最好使用 then
方法創(chuàng)建新的 Promises 的原因,如第二段代碼所示。它不僅看起來優(yōu)雅,并且還可以幫助我們避免那些極端情況。
串行調(diào)用 then 與并行調(diào)用 then 的比較
由于許多程序員都有著面向?qū)ο蟮木幊瘫尘?,因此?duì)他們來說,調(diào)用一個(gè)方法會(huì)更改一個(gè)對(duì)象,而非創(chuàng)建一個(gè)新的對(duì)象,這很稀松平常。
這或許也是我看到有人對(duì)于「在 Promise 上調(diào)用 then
方法時(shí)」到底發(fā)生了什么會(huì)感到困惑的原因。
比較下面兩段代碼:
const somePromise = createSomePromise();
somePromise
.then(doFirstThingWithResult)
.then(doSecondThingWithResult);
const somePromise = createSomePromise();
somePromise
.then(doFirstThingWithResult);
somePromise
.then(doSecondThingWithResult);
它們所做之事是否相同?看起來似乎相同,畢竟,兩段代碼都在 somePromise
上調(diào)用了兩次 then
,對(duì)嗎?
不,這又是一個(gè)非常普遍的誤區(qū)。實(shí)際上,這兩段代碼做的事情完全不同。如果不完全理解兩段代碼中正在做的事情,可能會(huì)導(dǎo)致出現(xiàn)非常棘手的錯(cuò)誤。
正如我們?cè)谥暗恼鹿?jié)中所說,then
方法會(huì)創(chuàng)建一個(gè)完全新的、獨(dú)立的 Promise。這意味著在第一段代碼中,第二個(gè) then
方法不是在 somePromise
上調(diào)用,而是在一個(gè)新的 Promise 對(duì)象上調(diào)用,這段代碼表示等待 somePromise
的狀態(tài)變?yōu)槌晒罅⒖陶{(diào)用 doFirstThingWithResult
。然后給新返回的 Promise 實(shí)例添加一個(gè)回調(diào)操作 doSecondThingWithResult
實(shí)際上,這兩個(gè)回調(diào)將會(huì)一個(gè)接著一個(gè)地執(zhí)行 —— 可以確保只有在第一個(gè)回調(diào)執(zhí)行完成且沒有任何問題之后,才會(huì)調(diào)用第二個(gè)回調(diào)。此外,第一個(gè)回調(diào)將會(huì)接收 somePromise
返回的值作為參數(shù),但是第二個(gè)回調(diào)函數(shù)將接收 doFirstThingWithResult
函數(shù)返回的值作為參數(shù)。
另一方面,在第二段代碼中,我們?cè)?somePromise
上調(diào)用兩次then
方法,基本上忽略了從該方法返回的兩個(gè)新的 Promises 對(duì)象。因?yàn)?then
在完全相同的 Promise 實(shí)例上被調(diào)用了兩次,因此我們無法確定首先執(zhí)行哪個(gè)回調(diào),這里的執(zhí)行順序是不確定的。
從某種意義上說,這兩個(gè)回調(diào)應(yīng)該是獨(dú)立的,并且不依賴于任何先前調(diào)用的回調(diào),我有時(shí)將其視為 “并行” 的執(zhí)行。但是,當(dāng)然,實(shí)際上,JS 引擎同一時(shí)刻只能執(zhí)行一個(gè)功能 —— 你根本無法知道它們將以什么順序調(diào)用。
兩段代碼的第二個(gè)不同之處是,在第二段代碼中 doFirstThingWithResult
和 doSecondThingWithResult
都會(huì)接收到同樣的參數(shù) —— somePromise
成功執(zhí)行返回的結(jié)果,兩個(gè)回調(diào)函數(shù)的返回值在這個(gè)示例中被完全忽略掉了。
創(chuàng)建后立即執(zhí)行 Promise
這個(gè)誤區(qū)出現(xiàn)的原因也是因?yàn)榇蟛糠殖绦騿T有著豐富的面向?qū)ο缶幊探?jīng)驗(yàn)。
在面向?qū)ο缶幊痰乃枷胫?,確保對(duì)象的構(gòu)造函數(shù)自身不執(zhí)行任何操作通常被認(rèn)為是一種很好的實(shí)踐。舉個(gè)例子,一個(gè)代表數(shù)據(jù)庫的對(duì)象在使用new
關(guān)鍵字調(diào)用其構(gòu)造函數(shù)時(shí)不應(yīng)該啟動(dòng)與數(shù)據(jù)庫的鏈接。
相反,應(yīng)該提供一個(gè)特定的方法,如調(diào)用一個(gè)名為 init
的方法 —— 它將顯式地創(chuàng)建連接。這樣,一個(gè)對(duì)象不會(huì)因?yàn)橐驯粍?chuàng)建而執(zhí)行任何期望之外的操作。它會(huì)按照程序員的明確要求來執(zhí)行。
但這「不是 Promises 的工作方式」。
考慮如下示例:
const somePromise = new Promise(resolve => {
// 創(chuàng)建 HTTP 請(qǐng)求
resolve(result);
});
你可能會(huì)認(rèn)為發(fā)出 HTTP 請(qǐng)求的函數(shù)未在此處調(diào)用,因?yàn)樗b在 Promise 構(gòu)造函數(shù)中。實(shí)際上,許多程序員希望 somePromise
上執(zhí)行 then
方法之后它才被調(diào)用。
但事實(shí)并非如此。創(chuàng)建該 Promise 后,回調(diào)將立即執(zhí)行。這意味著當(dāng)您在創(chuàng)建 somePromise
變量后進(jìn)入下一行時(shí),你的 HTTP 請(qǐng)求可能已被執(zhí)行,或者說已存在執(zhí)行隊(duì)列里。
我們說 Promise 是 “eager” 的,因?yàn)樗M可能快地執(zhí)行與其關(guān)聯(lián)的動(dòng)作。相反,許多人期望 Promises 是 “l(fā)azy” 的,即僅在必要時(shí)調(diào)用(例如,當(dāng) then
方法在 Promise 上首次被調(diào)用)。這是一個(gè)誤區(qū),Promise 永遠(yuǎn)是 eager 的,而非 lazy 的。
但是,如果您想要延遲執(zhí)行 Promise,應(yīng)該怎么做?如果您希望延遲發(fā)出該 HTTP 請(qǐng)求怎么辦?Promises 中是否內(nèi)置了某種奇特的機(jī)制,可以讓您執(zhí)行類似的操作?
答案有時(shí)會(huì)超出開發(fā)者們的期望。函數(shù)是一種 lazy 機(jī)制。僅當(dāng)程序員使用 ()
語法顯式調(diào)用它們時(shí),才執(zhí)行它們。僅僅定義一個(gè)函數(shù)實(shí)際上并不能做任何事情。因此,要使 Promise 成為 “l(fā)azy”, 最佳方法是將其簡(jiǎn)單地包裝在函數(shù)中!
具體代碼如下:
const createSomePromise = () => new Promise(resolve => {
// 創(chuàng)建 HTTP 請(qǐng)求
resolve(result);
});
現(xiàn)在,我們將 Promise 構(gòu)造函數(shù)的調(diào)用操作包裝在一個(gè)函數(shù)中。事實(shí)上它還沒有真正被調(diào)用。我們還將變量名從 somePromise
更改為 createSomePromise
,因?yàn)樗辉偈且粋€(gè) Promise 對(duì)象 —— 而是一個(gè)創(chuàng)建并返回 Promise 對(duì)象的函數(shù)。
Promise 構(gòu)造函數(shù)(以及帶有 HTTP 請(qǐng)求的回調(diào)函數(shù))僅在執(zhí)行該函數(shù)時(shí)被調(diào)用。因此,現(xiàn)在我們有了一個(gè) lazy 的 Promise,只有在我們真正想要它執(zhí)行時(shí)才去執(zhí)行它。
此外,請(qǐng)注意,它還附帶提供了另一種功能。我們可以輕松地創(chuàng)建另一個(gè)可以執(zhí)行相同操作的 Promise 對(duì)象。
如果出于某些奇怪的原因,我們希望進(jìn)行兩次相同的 HTTP 請(qǐng)求并同時(shí)執(zhí)行這些請(qǐng)求,則只需要兩次調(diào)用 createSomePromise
函數(shù)。又或者,如果請(qǐng)求由于任何原因失敗了,我們可以使用相同的函數(shù)重新請(qǐng)求。
這表明將 Promises 包裝在函數(shù)(或方法)中非常方便,因此對(duì)于 JavaScript 開發(fā)人員來說,使用這種模式開發(fā)應(yīng)該要變得很自然而然。
而諷刺的是,如果你閱讀過我寫的文章 Promises vs Observables ,你就會(huì)知道編寫 Rx.js 的程序員經(jīng)常會(huì)犯一個(gè)與此相反的錯(cuò)誤。他們對(duì) Observable 進(jìn)行編碼,就好像它們是 “eager”(與 Promises 一致),而實(shí)際上它們是 ”lazy“ 的。因此,將 Observables 封裝在函數(shù)或方法中通常沒有任何意義,實(shí)際上甚至是有害的。
結(jié)語
本文展示了我經(jīng)??吹介_發(fā)者使用 Promise 時(shí)所犯的三種類型的錯(cuò)誤,因?yàn)樗麄儗?duì) JavaScript 中的 Promises 的理解僅停留在表面。
以上就是W3Cschool編程獅
關(guān)于在 JavaScript 中使用 Promises 時(shí)最常見的 3 個(gè)錯(cuò)誤的相關(guān)介紹了,希望對(duì)大家有所幫助。