App下載

在 JavaScript 中使用 Promises 時(shí)最常見的 3 個(gè)錯(cuò)誤

猿友 2020-09-03 14:36:17 瀏覽數(shù) (2529)
反饋

文章轉(zhuǎn)載自公眾號:印記中文

原文鏈接:dev.to/mpodlasin/3-most-common-mistakes-when-using-promises-in-javascript-oab

譯者:Shopee 金融前端團(tuán)隊(duì) 張鐵山

本文對開發(fā)者編寫 Promise 時(shí)常出現(xiàn)的幾種錯(cuò)誤進(jìn)行了總結(jié),剖析的一針見血,來看看是不是你平時(shí)所寫?

時(shí)至今日,即使有 async / await 的引入,JavaScriptPromises 的編寫規(guī)則對于所有的 JS 開發(fā)者來說仍然是必不可少的知識。

JavaScript 在處理異步問題上和其它編程語言不同。因此,即使具有豐富經(jīng)驗(yàn)的開發(fā)人員有時(shí)也會陷入誤區(qū)。我親身看到過優(yōu)秀的 PythonJava 程序員在為 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í),你會了解到 Promise 的構(gòu)造函數(shù),這個(gè)構(gòu)造函數(shù)可以用來創(chuàng)建一個(gè)新的 Promises 對象。

也許因?yàn)槿藗兺ǔJ峭ㄟ^將一些瀏覽器 API(例如 setTimeout)包裝在 Promise 構(gòu)造函數(shù)中這種方式來開始學(xué)習(xí)的,所以在他們心中根深蒂固地認(rèn)為創(chuàng)建 Promise 對象的唯一方法是使用構(gòu)造函數(shù)。

因此,通常會這樣寫:

const createdPromise = new Promise(resolve => {
  somePreviousPromise.then(result => {
    // 對 result 進(jìn)行一些操作
    resolve(result);
  });
});

可以看到,為了對 somePreviousPromise 的結(jié)果 result 進(jìn)行一些操作,有些人使用了 then,但是后來決定將其再次包裝在一個(gè) Promise 的構(gòu)造函數(shù)中,為的是將該操作的結(jié)果存儲在 createdPromise 的變量中,大概是為了稍后對該 Promise 進(jìn)行更多操作。

這顯然是沒有必要的。then 方法的全部要點(diǎn)在于它本身會返回一個(gè) Promise,它表示的是執(zhí)行 somePreviousPromise 后再執(zhí)行then 中的的回調(diào)函數(shù),then 的參數(shù)是 somePreviousPromise成功執(zhí)行返回的結(jié)果。

所以,上一段代碼大致等價(jià)于:

const createPromise = somePreviousPromise.then(result => {
  // 對 result 進(jìn)行一些操作
  return result
})

如此編寫,會簡潔很多。

但是,為什么我說它只是大致等價(jià)呢?區(qū)別在哪里?

經(jīng)驗(yàn)不足且不細(xì)心觀察的話可能很難發(fā)現(xiàn),實(shí)際上兩者在錯(cuò)誤處理上存在巨大的差異,這種差異比第一段代碼的冗余問題更為重要。

假設(shè) somePreviousPromise 出于某些原因失敗了且拋出錯(cuò)誤。例如,這個(gè) Promise 里發(fā)送了一個(gè) HTTP 請求,而 API 響應(yīng) 500 錯(cuò)誤。

事實(shí)證明,在上一段代碼中,我們將一個(gè) Promise 包裝到另一個(gè) Promise 中,我們根本無法捕獲該錯(cuò)誤。為了解決此問題,我們必須進(jìn)行以下更改:

const createdPromise = new Promise((resolve, reject) => {
  somePreviousPromise.then(result => {
    // 對 result 進(jìn)行一些操作
    resolve(result);
  }, reject);
});

我們簡單的在回調(diào)函數(shù)中添加了一個(gè) reject 參數(shù),然后通過將其作為第二個(gè)參數(shù)傳遞給 then 的方式來使用它。請務(wù)必記住,then 方法接受第二個(gè)可選參數(shù)來進(jìn)行錯(cuò)誤處理,這一點(diǎn)非常重要。

現(xiàn)在如果 somePreviousPromise 出于某些原因失敗了,reject函數(shù)將會被調(diào)用,并且我們將能夠一如往常地處理 createdPromise上的錯(cuò)誤。

這樣是否解決了所有問題?抱歉,并沒有。

我們處理了 somePreviousPromise 自身可能發(fā)生的錯(cuò)誤,但是我們?nèi)匀粺o法控制作為 then 方法第一個(gè)參數(shù)的回調(diào)函數(shù)中發(fā)生的情況。在注釋區(qū)域 // 對 result 進(jìn)行一些操作 執(zhí)行的代碼可能會有一些錯(cuò)誤,如果這塊地方的代碼拋出任何錯(cuò)誤,那 then 方法的第二個(gè)參數(shù)reject 依舊捕獲不到這些錯(cuò)誤。

這是因?yàn)樽鳛?then 方法的第二個(gè)參數(shù)的錯(cuò)誤處理函數(shù)只對 Promise 鏈上當(dāng)前 then 之前發(fā)生的錯(cuò)誤作出響應(yīng)。

因此,最合適的(也是最終的)解決方案應(yīng)該如下:

const createdPromise = new Promise((resolve, reject) => {
  somePreviousPromise.then(result => {
    // 對 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ū)ο蟮木幊瘫尘?,因此對他們來說,調(diào)用一個(gè)方法會更改一個(gè)對象,而非創(chuàng)建一個(gè)新的對象,這很稀松平常。

這或許也是我看到有人對于「在 Promise 上調(diào)用 then 方法時(shí)」到底發(fā)生了什么會感到困惑的原因。

比較下面兩段代碼:

const somePromise = createSomePromise();


somePromise
  .then(doFirstThingWithResult)
  .then(doSecondThingWithResult);
const somePromise = createSomePromise();


somePromise
  .then(doFirstThingWithResult);


somePromise
  .then(doSecondThingWithResult);

它們所做之事是否相同?看起來似乎相同,畢竟,兩段代碼都在 somePromise 上調(diào)用了兩次 then,對嗎?

不,這又是一個(gè)非常普遍的誤區(qū)。實(shí)際上,這兩段代碼做的事情完全不同。如果不完全理解兩段代碼中正在做的事情,可能會導(dǎo)致出現(xiàn)非常棘手的錯(cuò)誤。

正如我們在之前的章節(jié)中所說,then 方法會創(chuàng)建一個(gè)完全新的、獨(dú)立的 Promise。這意味著在第一段代碼中,第二個(gè) then 方法不是在 somePromise 上調(diào)用,而是在一個(gè)新的 Promise 對象上調(diào)用,這段代碼表示等待 somePromise 的狀態(tài)變?yōu)槌晒罅⒖陶{(diào)用 doFirstThingWithResult。然后給新返回的 Promise 實(shí)例添加一個(gè)回調(diào)操作 doSecondThingWithResult

實(shí)際上,這兩個(gè)回調(diào)將會一個(gè)接著一個(gè)地執(zhí)行 —— 可以確保只有在第一個(gè)回調(diào)執(zhí)行完成且沒有任何問題之后,才會調(diào)用第二個(gè)回調(diào)。此外,第一個(gè)回調(diào)將會接收 somePromise 返回的值作為參數(shù),但是第二個(gè)回調(diào)函數(shù)將接收 doFirstThingWithResult 函數(shù)返回的值作為參數(shù)。

另一方面,在第二段代碼中,我們在 somePromise 上調(diào)用兩次then 方法,基本上忽略了從該方法返回的兩個(gè)新的 Promises 對象。因?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è)不同之處是,在第二段代碼中 doFirstThingWithResultdoSecondThingWithResult 都會接收到同樣的參數(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ū)ο缶幊痰乃枷胫?,確保對象的構(gòu)造函數(shù)自身不執(zhí)行任何操作通常被認(rèn)為是一種很好的實(shí)踐。舉個(gè)例子,一個(gè)代表數(shù)據(jù)庫的對象在使用new 關(guān)鍵字調(diào)用其構(gòu)造函數(shù)時(shí)不應(yīng)該啟動與數(shù)據(jù)庫的鏈接。

相反,應(yīng)該提供一個(gè)特定的方法,如調(diào)用一個(gè)名為 init 的方法 —— 它將顯式地創(chuàng)建連接。這樣,一個(gè)對象不會因?yàn)橐驯粍?chuàng)建而執(zhí)行任何期望之外的操作。它會按照程序員的明確要求來執(zhí)行。

但這「不是 Promises 的工作方式」

考慮如下示例:

const somePromise = new Promise(resolve => {
  // 創(chuàng)建 HTTP 請求
  resolve(result);
});

你可能會認(rèn)為發(fā)出 HTTP 請求的函數(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 請求可能已被執(zhí)行,或者說已存在執(zhí)行隊(duì)列里。

我們說 Promise 是 “eager” 的,因?yàn)樗M可能快地執(zhí)行與其關(guān)聯(lián)的動作。相反,許多人期望 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 請求怎么辦?Promises 中是否內(nèi)置了某種奇特的機(jī)制,可以讓您執(zhí)行類似的操作?

答案有時(shí)會超出開發(fā)者們的期望。函數(shù)是一種 lazy 機(jī)制。僅當(dāng)程序員使用 () 語法顯式調(diào)用它們時(shí),才執(zhí)行它們。僅僅定義一個(gè)函數(shù)實(shí)際上并不能做任何事情。因此,要使 Promise 成為 “l(fā)azy”, 最佳方法是將其簡單地包裝在函數(shù)中!

具體代碼如下:

const createSomePromise = () => new Promise(resolve => {
  // 創(chuàng)建 HTTP 請求
  resolve(result);
});

現(xiàn)在,我們將 Promise 構(gòu)造函數(shù)的調(diào)用操作包裝在一個(gè)函數(shù)中。事實(shí)上它還沒有真正被調(diào)用。我們還將變量名從 somePromise 更改為 createSomePromise,因?yàn)樗辉偈且粋€(gè) Promise 對象 —— 而是一個(gè)創(chuàng)建并返回 Promise 對象的函數(shù)。

Promise 構(gòu)造函數(shù)(以及帶有 HTTP 請求的回調(diào)函數(shù))僅在執(zhí)行該函數(shù)時(shí)被調(diào)用。因此,現(xiàn)在我們有了一個(gè) lazy 的 Promise,只有在我們真正想要它執(zhí)行時(shí)才去執(zhí)行它。

此外,請注意,它還附帶提供了另一種功能。我們可以輕松地創(chuàng)建另一個(gè)可以執(zhí)行相同操作的 Promise 對象。

如果出于某些奇怪的原因,我們希望進(jìn)行兩次相同的 HTTP 請求并同時(shí)執(zhí)行這些請求,則只需要兩次調(diào)用 createSomePromise 函數(shù)。又或者,如果請求由于任何原因失敗了,我們可以使用相同的函數(shù)重新請求。

這表明將 Promises 包裝在函數(shù)(或方法)中非常方便,因此對于 JavaScript 開發(fā)人員來說,使用這種模式開發(fā)應(yīng)該要變得很自然而然。

而諷刺的是,如果你閱讀過我寫的文章 Promises vs Observables ,你就會知道編寫 Rx.js 的程序員經(jīng)常會犯一個(gè)與此相反的錯(cuò)誤。他們對 Observable 進(jìn)行編碼,就好像它們是 “eager”(與 Promises 一致),而實(shí)際上它們是 ”lazy“ 的。因此,將 Observables 封裝在函數(shù)或方法中通常沒有任何意義,實(shí)際上甚至是有害的。

結(jié)語

本文展示了我經(jīng)??吹介_發(fā)者使用 Promise 時(shí)所犯的三種類型的錯(cuò)誤,因?yàn)樗麄儗?JavaScript 中的 Promises 的理解僅停留在表面。

以上就是W3Cschool編程獅關(guān)于在 JavaScript 中使用 Promises 時(shí)最常見的 3 個(gè)錯(cuò)誤的相關(guān)介紹了,希望對大家有所幫助。

0 人點(diǎn)贊