App下載

在 JavaScript 中使用 Promises 時最常見的 3 個錯誤

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

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

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

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

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

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

JavaScript 在處理異步問題上和其它編程語言不同。因此,即使具有豐富經(jīng)驗的開發(fā)人員有時也會陷入誤區(qū)。我親身看到過優(yōu)秀的 PythonJava 程序員在為 Node.js 或瀏覽器編碼時犯了非常愚蠢的錯誤。

為了避免這些錯誤,JavaScript 中的 Promises 有許多編寫細(xì)節(jié)需要考慮。其中有一些純粹是語言風(fēng)格問題,但也有許多是實際引入、難以跟蹤的錯誤。因此,我決定編寫一個清單,列出開發(fā)人員在使用 Promises 編程時遇到的三個最常見的錯誤。

將所有內(nèi)容包裝在 Promise 構(gòu)造函數(shù)中

第一個錯誤也是最為明顯的錯誤之一,但是我發(fā)現(xiàn)開發(fā)者犯這個錯誤的頻率出奇的高。

當(dāng)?shù)谝淮螌W(xué)習(xí) Promises 時,你會了解到 Promise 的構(gòu)造函數(shù),這個構(gòu)造函數(shù)可以用來創(chuàng)建一個新的 Promises 對象。

也許因為人們通常是通過將一些瀏覽器 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,但是后來決定將其再次包裝在一個 Promise 的構(gòu)造函數(shù)中,為的是將該操作的結(jié)果存儲在 createdPromise 的變量中,大概是為了稍后對該 Promise 進(jìn)行更多操作。

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

所以,上一段代碼大致等價于:

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

如此編寫,會簡潔很多。

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

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

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

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

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

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

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

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

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

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

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

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

注意,這次我們使用了 catch 方法 —— 因為它將在第一個 then之后被調(diào)用,它將捕獲到 Promise 鏈上拋出的所有錯誤。無論是 somePreviousPromise 還是 then 中的回調(diào)失敗了,Promise 都將按預(yù)期處理這些情況。

從上述示例可以發(fā)現(xiàn),在 Promise 的構(gòu)造函數(shù)中包裝代碼時,有很多細(xì)節(jié)問題需要處理。這就是為什么最好使用 then 方法創(chuàng)建新的 Promises 的原因,如第二段代碼所示。它不僅看起來優(yōu)雅,并且還可以幫助我們避免那些極端情況。

串行調(diào)用 then 與并行調(diào)用 then 的比較

由于許多程序員都有著面向?qū)ο蟮木幊瘫尘?,因此對他們來說,調(diào)用一個方法會更改一個對象,而非創(chuàng)建一個新的對象,這很稀松平常。

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

比較下面兩段代碼:

const somePromise = createSomePromise();


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


somePromise
  .then(doFirstThingWithResult);


somePromise
  .then(doSecondThingWithResult);

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

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

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

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

另一方面,在第二段代碼中,我們在 somePromise 上調(diào)用兩次then 方法,基本上忽略了從該方法返回的兩個新的 Promises 對象。因為 then 在完全相同的 Promise 實例上被調(diào)用了兩次,因此我們無法確定首先執(zhí)行哪個回調(diào),這里的執(zhí)行順序是不確定的。

從某種意義上說,這兩個回調(diào)應(yīng)該是獨立的,并且不依賴于任何先前調(diào)用的回調(diào),我有時將其視為 “并行” 的執(zhí)行。但是,當(dāng)然,實際上,JS 引擎同一時刻只能執(zhí)行一個功能 —— 你根本無法知道它們將以什么順序調(diào)用。

兩段代碼的第二個不同之處是,在第二段代碼中 doFirstThingWithResultdoSecondThingWithResult 都會接收到同樣的參數(shù) —— somePromise 成功執(zhí)行返回的結(jié)果,兩個回調(diào)函數(shù)的返回值在這個示例中被完全忽略掉了。

創(chuàng)建后立即執(zhí)行 Promise

這個誤區(qū)出現(xiàn)的原因也是因為大部分程序員有著豐富的面向?qū)ο缶幊探?jīng)驗。

在面向?qū)ο缶幊痰乃枷胫?,確保對象的構(gòu)造函數(shù)自身不執(zhí)行任何操作通常被認(rèn)為是一種很好的實踐。舉個例子,一個代表數(shù)據(jù)庫的對象在使用new 關(guān)鍵字調(diào)用其構(gòu)造函數(shù)時不應(yīng)該啟動與數(shù)據(jù)庫的鏈接。

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

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

考慮如下示例:

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

你可能會認(rèn)為發(fā)出 HTTP 請求的函數(shù)未在此處調(diào)用,因為它包裝在 Promise 構(gòu)造函數(shù)中。實際上,許多程序員希望 somePromise 上執(zhí)行 then 方法之后它才被調(diào)用。

但事實并非如此。創(chuàng)建該 Promise 后,回調(diào)將立即執(zhí)行。這意味著當(dāng)您在創(chuàng)建 somePromise 變量后進(jìn)入下一行時,你的 HTTP 請求可能已被執(zhí)行,或者說已存在執(zhí)行隊列里。

我們說 Promise 是 “eager” 的,因為它盡可能快地執(zhí)行與其關(guān)聯(lián)的動作。相反,許多人期望 Promises 是 “l(fā)azy” 的,即僅在必要時調(diào)用(例如,當(dāng) then 方法在 Promise 上首次被調(diào)用)。這是一個誤區(qū),Promise 永遠(yuǎn)是 eager 的,而非 lazy 的。

但是,如果您想要延遲執(zhí)行 Promise,應(yīng)該怎么做?如果您希望延遲發(fā)出該 HTTP 請求怎么辦?Promises 中是否內(nèi)置了某種奇特的機(jī)制,可以讓您執(zhí)行類似的操作?

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

具體代碼如下:

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

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

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

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

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

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

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

結(jié)語

本文展示了我經(jīng)??吹介_發(fā)者使用 Promise 時所犯的三種類型的錯誤,因為他們對 JavaScript 中的 Promises 的理解僅停留在表面。

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

0 人點贊