摘要:盡管可以讓代碼更加簡潔易讀,但對于只熟悉回調(diào)函數(shù)的人來說,可能對此還是會有所懷疑。始終避免在或使用回調(diào)函數(shù),否則會吞噬任何后續(xù)的錯誤,將其作為鏈的一部分。然而,使用回調(diào)函數(shù),使用所謂的,即第一個參數(shù)是一個錯誤回調(diào)變得很常見。
原文:ES6 Promises: Patterns and Anti-Patterns
作者:Bobby Brennan
當(dāng)幾年前,第一次使用 NodeJS 的時候,對現(xiàn)在被稱為“ 回調(diào)地獄 ”的寫法感到很困擾。幸運的是,現(xiàn)在是 2017 年了,NodeJS 已經(jīng)采用大量 JavaScript 的最新特性,從 v4 開始已經(jīng)支持 Promise。
盡管 Promise 可以讓代碼更加簡潔易讀,但對于只熟悉回調(diào)函數(shù)的人來說,可能對此還是會有所懷疑。在這里,將列出我在使用Promise 時學(xué)到的一些基本模式,以及踩的一些坑。
注意:在本文中將使用箭頭函數(shù) ,如果你還不是很熟悉,其實很簡單,建議先讀一下使用它們的好處
模式與最佳實踐 使用 Promise如果使用的是已經(jīng)支持 Promise 的第三方庫,那么使用起來非常簡單。只需關(guān)心兩個函數(shù):then() 和 catch()。例如,有一個客戶端 API 包含三個方法,getItem(),updateItem(),和deleteItem(),每一個方法都返回一個 Promise:
Promise.resolve() .then(_ => { return api.getItem(1) }) .then(item => { item.amount++ return api.updateItem(1, item); }) .then(update => { return api.deleteItem(1); }) .catch(e => { console.log("error while working on item 1"); })
每次調(diào)用 then() 會在 Promise 鏈中創(chuàng)建一個新的步驟,如果鏈中的任何一個地方出現(xiàn)錯誤,就會觸發(fā)接下來的 catch() 。then() 和 catch() 都可以返回一個值或者一個新的 Promise,結(jié)果將被傳遞到 Promise 鏈的下一個then()。
為了比較,這里使用回調(diào)函數(shù)來實現(xiàn)相同邏輯:
api.getItem(1, (err, data) => { if (err) throw err; item.amount++; api.updateItem(1, item, (err, update) => { if (err) throw err; api.deleteItem(1, (err) => { if (err) throw err; }) }) })
要注意的第一個區(qū)別是,使用回調(diào)函數(shù),我們必須在過程的每個步驟中進行錯誤處理,而不是用單個的 catch-all 來處理?;卣{(diào)函數(shù)的第二個問題更直觀,每個步驟都要水平縮進,而使用 Promise 的代碼則有顯而易見的順序關(guān)系。
回調(diào)函數(shù) Promise 化需要學(xué)習(xí)的第一個技巧是如何將回調(diào)函數(shù)轉(zhuǎn)換為 Promise。你可能正在使用仍然基于回調(diào)的庫,或是自己的舊代碼,不過不用擔(dān)心,因為只需要幾行代碼就可以將其包裝成一個 Promise。這是將 Node 中的一個回調(diào)方法 fs.readFile 轉(zhuǎn)換為 Promise的示例:
function readFilePromise(filename) { return new Promise((resolve, reject) => { fs.readFile(filename, "utf8", (err, data) => { if (err) reject(err); else resolve(data); }) }) } readFilePromise("index.html") .then(data => console.log(data)) .catch(e => console.log(e))
關(guān)鍵部分是 Promise 構(gòu)造函數(shù),它接收一個函數(shù)作為參數(shù),這個函數(shù)有兩個函數(shù)參數(shù):resolve 和 reject。在這個函數(shù)里完成所有工作,完成之后,在成功時調(diào)用 resolve,如果有錯誤則調(diào)用 reject。
需要注意的是只有一個resolve 或者 reject 被調(diào)用,即應(yīng)該只被調(diào)用一次。在我們的示例中,如果 fs.readFile 返回錯誤,我們將錯誤傳遞給 reject,否則將文件數(shù)據(jù)傳遞給resolve。
Promise 的值ES6 有兩個很方便的輔助函數(shù),用于通過普通值創(chuàng)建 Promise:Promise.resolve() 和 Promise.reject()。例如,可能需要在同步處理某些情況時一個返回 Promise 的函數(shù):
function readFilePromise(filename) { if (!filename) { return Promise.reject(new Error("Filename not specified")); } if (filename === "index.html") { return Promise.resolve("Hello!
"); } return new Promise((resolve, reject) => {/*...*/}) }
注意,雖然可以傳遞任何東西(或者不傳遞任何值)給 Promise.reject(),但是好的做法是傳遞一個Error。
并行運行Promise.all是一個并行運行 Promise 數(shù)組的方法,也就是說是同時運行。例如,我們有一個要從磁盤讀取文件的列表。使用上面創(chuàng)建的 readFilePromise 函數(shù),將如下所示:
let filenames = ["index.html", "blog.html", "terms.html"]; Promise.all(filenames.map(readFilePromise)) .then(files => { console.log("index:", files[0]); console.log("blog:", files[1]); console.log("terms:", files[2]); })
我甚至不會使用傳統(tǒng)的回調(diào)函數(shù)來嘗試編寫與之等效的代碼,那樣會很凌亂,而且也容易出錯。
串行運行有時同時運行一堆 Promise 可能會出現(xiàn)問題。比如,如果嘗試使用 Promise.all 的 API ??去檢索一堆資源,則可能會在達到速率限制時開始響應(yīng)429錯誤。
一種解決方案是串行運行 Promise,或一個接一個地運行。但是在 ES6 中沒有提供類似 Promise.all 這樣的方法(為什么?),但我們可以使用 Array.reduce 來實現(xiàn):
let itemIDs = [1, 2, 3, 4, 5]; itemIDs.reduce((promise, itemID) => { return promise.then(_ => api.deleteItem(itemID)); }, Promise.resolve());
在這種情況下,我們需要等待每次調(diào)用 api.deleteItem() 完成之后才能進行下一次調(diào)用。這種方法,比為每個 itemID 寫 .then() 更簡潔更通用:
Promise.resolve() .then(_ => api.deleteItem(1)) .then(_ => api.deleteItem(2)) .then(_ => api.deleteItem(3)) .then(_ => api.deleteItem(4)) .then(_ => api.deleteItem(5));Race
ES6 提供的另一個很方便的函數(shù)是 Promise.race。跟 Promise.all 一樣,接收一個 Promise 數(shù)組,并同時運行它們,但不同的是,會在一旦任何 Promise 完成或失敗的情況下返回,并放棄所有其他的結(jié)果。
例如,我們可以創(chuàng)建一個在幾秒鐘之后超時的 Promise:
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(reject, ms); }) } Promise.race([readFilePromise("index.html"), timeout(1000)]) .then(data => console.log(data)) .catch(e => console.log("Timed out after 1 second"))
需要注意的是,其他 Promise 仍將繼續(xù)運行 ,只是看不到結(jié)果而已。
捕獲錯誤捕獲錯誤最常見的方式是添加一個 .catch() 代碼塊,這將捕獲前面所有 .then() 代碼塊中的錯誤 :
Promise.resolve() .then(_ => api.getItem(1)) .then(item => { item.amount++; return api.updateItem(1, item); }) .catch(e => { console.log("failed to get or update item"); })
在這里,只要有 getItem 或者 updateItem 失敗,catch()就會被觸發(fā)。但是如果我們想分開處理 getItem 的錯誤怎么辦?只需再插入一個catch() 就可以,它也可以返回另一個 Promise。
Promise.resolve() .then(_ => api.getItem(1)) .catch(e => api.createItem(1, {amount: 0})) .then(item => { item.amount++; return api.updateItem(1, item); }) .catch(e => { console.log("failed to update item"); })
現(xiàn)在,如果getItem()失敗,我們通過第一個 catch 介入并創(chuàng)建一條新的記錄。
拋出錯誤應(yīng)該將 then() 語句中的所有代碼視為 try 塊內(nèi)的所有代碼。return Promise.reject() 和 throw new Error() 都會導(dǎo)致下一個 catch() 代碼塊的運行。
這意味著運行時錯誤也會觸發(fā) catch(),所以不要去假設(shè)錯誤的來源。例如,在下面的代碼中,我們可能希望該 catch() 只能獲得 getItem 拋出的錯誤,但是如示例所示,它還會在我們的 then() 語句中捕獲運行時錯誤。
api.getItem(1) .then(item => { delete item.owner; console.log(item.owner.name); }) .catch(e => { console.log(e); // Cannot read property "name" of undefined })動態(tài)鏈
有時,我們想要動態(tài)地構(gòu)建 Promise 鏈,例如,在滿足特定條件時,插入一個額外的步驟。在下面的示例中,在讀取給定文件之前,我們可以選擇創(chuàng)建一個鎖定文件:
function readFileAndMaybeLock(filename, createLockFile) { let promise = Promise.resolve(); if (createLockFile) { promise = promise.then(_ => writeFilePromise(filename + ".lock", "")) } return promise.then(_ => readFilePromise(filename)); }
一定要通過重寫 promise = promise.then(/*...*/) 來更新 Promise 的值。參看接下來反模式中會提到的 多次調(diào)用 then()。
反模式Promise 是一個整潔的抽象,但很容易陷入某些陷阱。以下是我遇到的一些最常見的問題。
重回回調(diào)地獄當(dāng)我第一次從回調(diào)函數(shù)轉(zhuǎn)到 Promise 時,發(fā)現(xiàn)很難擺脫一些舊習(xí)慣,仍像使用回調(diào)函數(shù)一樣嵌套 Promise:
api.getItem(1) .then(item => { item.amount++; api.updateItem(1, item) .then(update => { api.deleteItem(1) .then(deletion => { console.log("done!"); }) }) })
這種嵌套是完全沒有必要的。有時一兩層嵌套可以幫助組合相關(guān)任務(wù),但是最好總是使用 .then() 重寫成 Promise 垂直鏈 。
沒有返回我遇到的一個經(jīng)常會犯的錯誤是在一個 Promise 鏈中忘記 return 語句。你能發(fā)現(xiàn)下面的 bug 嗎?
api.getItem(1) .then(item => { item.amount++; api.updateItem(1, item); }) .then(update => { return api.deleteItem(1); }) .then(deletion => { console.log("done!"); })
因為我們沒有在第4行的 api.updateItem() 前面寫 return,所以 then() 代碼塊會立即 resolove,導(dǎo)致 api.deleteItem() 可能在 api.updateItem() 完成之前就被調(diào)用。
在我看來,這是 ES6 Promise 的一個大問題,往往會引發(fā)意想不到的行為。問題是, .then() 可以返回一個值,也可以返回一個新的 Promise,undefined 完全是一個有效的返回值。就個人而言,如果我負責(zé) Promise API,我會在 .then() 返回 undefined 時拋出運行時錯誤,但現(xiàn)在我們需要特別注意 return 創(chuàng)建的 Promise。
多次調(diào)用 .then()根據(jù)規(guī)范,在同一個 Promise 上多次調(diào)用 then() 是完全有效的,并且回調(diào)將按照其注冊順序被調(diào)用。但是,我并未見過需要這樣做的場景,并且在使用返回值和錯誤處理時可能會產(chǎn)生一些意外行為:
let p = Promise.resolve("a"); p.then(_ => "b"); p.then(result => { console.log(result) // "a" }) let q = Promise.resolve("a"); q = q.then(_ => "b"); q = q.then(result => { console.log(result) // "b" })
在這個例子中,因為我們在每次調(diào)用 then() 不更新 p 的值,所以我們看不到 "b" 返回。但是每次調(diào)用 then() 時更新 q,所以其行為更可預(yù)測。
這也適用于錯誤處理:
let p = Promise.resolve(); p.then(_ => {throw new Error("whoops!")}) p.then(_ => { console.log("hello!"); // "hello!" }) let q = Promise.resolve(); q = q.then(_ => {throw new Error("whoops!")}) q = q.then(_ => { console.log("hello"); // We never reach here })
在這里,我們期望的是拋出一個錯誤來打破 Promise 鏈,但由于沒有更新 p 的值,所以第二個 then() 仍會被調(diào)用。
有可能在一個 Promise 上多次調(diào)用 .then() 有很多理由 ,因為它允許將 Promise 分配到幾個新的獨立的 Promise 中,但是還沒發(fā)現(xiàn)真實的使用場景。
混合使用回調(diào)和 Promise很容易進入一種陷阱,在使用基于 Promise 庫的同時,仍在基于回調(diào)的項目中工作。始終避免在 then() 或 catch() 使用回調(diào)函數(shù)?,否則 Promise 會吞噬任何后續(xù)的錯誤,將其作為 Promise 鏈的一部分。例如,以下內(nèi)容看起來是一個挺合理的方式,使用回調(diào)函數(shù)來包裝一個 Promise:
function getThing(callback) { api.getItem(1) .then(item => callback(null, item)) .catch(e => callback(e)); } getThing(function(err, thing) { if (err) throw err; console.log(thing); })
這里的問題是,如果有錯誤,我們會收到關(guān)于“Unhandled promise rejection”的警告,即使我們添加了一個 catch() 代碼塊。這是因為,callback() 在 then() 和 catch() 都會被調(diào)用,使之成為 Promise 鏈的一部分。
如果必須使用回調(diào)來包裝 Promise,可以使用 setTimeout (或者是 NodeJS 中的 process.nextTick)來打破 Promise:
function getThing(callback) { api.getItem(1) .then(item => setTimeout(_ => callback(null, item))) .catch(e => setTimeout(_ => callback(e))); } getThing(function(err, thing) { if (err) throw err; console.log(thing); })不捕獲錯誤
JavaScript 中的錯誤處理有點奇怪。雖然支持熟悉的 try/catch 范例,但是沒有辦法強制調(diào)用者以 Java 的方式處理錯誤。然而,使用回調(diào)函數(shù),使用所謂的“errbacks”,即第一個參數(shù)是一個錯誤回調(diào)變得很常見。這迫使調(diào)用者至少承認(rèn)錯誤的可能性。例如,fs 庫:
fs.readFile("index.html", "utf8", (err, data) => { if (err) throw err; console.log(data); })
使用 Promise,又將很容易忘記需要進行錯誤處理,特別是對于敏感操作(如文件系統(tǒng)和數(shù)據(jù)庫訪問)。目前,如果沒有捕獲到 reject 的 Promise,將在 NodeJS 中看到非常丑的警告:
(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops! (node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
確保在主要的事件循環(huán)中任何 Promise 鏈的末尾添加 catch() 以避免這種情況。
總結(jié)希望這是一篇有用的關(guān)于常見 Promise 模式和反模式的概述。如果你想了解更多,這里有一些有用的資源:
Mozilla 的 ES6 Promise 文檔
來自 Google 的 Promise 介紹
Dave Atchley 的 ES6 Promise 概述
更多的 Promise 模式和反模式
或者閱讀來自 DataFire 團隊的內(nèi)容
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/88776.html
摘要:前端日報精選劉海打理指北中的錯誤處理模式與反模式譯圖解和譯你并不知道中文裝飾器讓你的代碼更簡潔眾成翻譯第期每個程序員第一份工作前應(yīng)該知道的件事中的不變性眾成翻譯寫的一次小結(jié)掘金內(nèi)部機制探秘和文末附彩蛋和源碼前端雜談開發(fā)實戰(zhàn) 2017-09-30 前端日報 精選 iPhone X 劉海打理指北React16中的錯誤處理ES6 Promise:模式與反模式「譯」圖解 ArrayBuffer...
摘要:回調(diào)函數(shù)這是異步編程最基本的方法。對象對象是工作組提出的一種規(guī)范,目的是為異步編程提供統(tǒng)一接口。誕生后,出現(xiàn)了函數(shù),它將異步編程帶入了一個全新的階段。 更多詳情點擊http://blog.zhangbing.club/Ja... Javascript 語言的執(zhí)行環(huán)境是單線程的,如果沒有異步編程,根本沒法用,非卡死不可。 為了解決這個問題,Javascript語言將任務(wù)的執(zhí)行模式分成兩種...
摘要:的翻譯文檔由的維護很多人說,阮老師已經(jīng)有一本關(guān)于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發(fā)過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:又譬如在一個多圖展示的網(wǎng)頁,由于圖片過多或圖片太大,我們希望圖片加載完再一次性顯示,而不是東一張西一張陸續(xù)顯示,這時候也需要用圖片一次性加載功能。 基于promise的圖片資源一次性加載或者預(yù)加載 作者:NEXT卓 場景描述 不是每個網(wǎng)頁端的用戶都能用得起光纖,不是每張圖片都是壓縮得很小,有時候我們也想要看高清大圖,但是受限于網(wǎng)速有時候場景是這樣的:(很明顯左邊的第一張圖片還沒出來,其...
摘要:又譬如在一個多圖展示的網(wǎng)頁,由于圖片過多或圖片太大,我們希望圖片加載完再一次性顯示,而不是東一張西一張陸續(xù)顯示,這時候也需要用圖片一次性加載功能。 基于promise的圖片資源一次性加載或者預(yù)加載 作者:NEXT卓 場景描述 不是每個網(wǎng)頁端的用戶都能用得起光纖,不是每張圖片都是壓縮得很小,有時候我們也想要看高清大圖,但是受限于網(wǎng)速有時候場景是這樣的:(很明顯左邊的第一張圖片還沒出來,其...
閱讀 3739·2021-10-14 09:43
閱讀 3323·2021-08-25 09:38
閱讀 618·2019-08-30 15:55
閱讀 1362·2019-08-30 13:05
閱讀 2254·2019-08-29 16:05
閱讀 517·2019-08-29 12:58
閱讀 2804·2019-08-29 12:34
閱讀 3255·2019-08-26 12:15