【譯】JavaScript中的Promises
你有沒有在JavaScript中遇到過 promises
並想知道它們是什麼?它們為什麼會被稱為 promises
呢?它們是否和你以任何方式對另一個人做出的承諾有關呢?
此外,你為什麼要使用 promises
呢?與傳統的JavaScript操作回撥(callbacks)相比,它們有什麼好處呢?
在本文中,你將學習有關JavaScript中 promises
的所有內容。你將明白它們是什麼,怎麼去使用它們,以及為什麼它們比回撥更受歡迎。
所以,promise是什麼?
promise是一個將來會返回值的物件。由於這種 未來
的東西, Promises 非常適合非同步JavaScript操作。
如果你不明白非同步JavaScript意味著什麼,你可能還不適合讀這篇文章。我建議你回到 關於callbacks這篇文章 瞭解後再回來。
通過類比會更好地解析JavaScript promise
的概念,所以我們來這樣做(類比),使其概念更加清晰。
想象一下,你準備下周為你的侄女舉辦生日派對。當你談到派對時,你的朋友,Jeff,提出他可以提供幫助。你很高心,讓他買一個黑森林(風格的)生日蛋糕。Jeff說可以。
在這裡,Jeff告訴你他會給你買一個黑森林生日蛋糕。這是約定好的。在JavaScript中, promise
的工作方式和現實生活中的承諾一樣。可以使用以下方式編寫JavaScript版本的場景:
// jeffBuysCake is a promise const promise = jeffBuysCake('black forest')
你將學習如何構建 jeffBuysCake
。現在,把它當成一個 promise
。
現在,Jeff尚未採取行動。在JavaScript中,我們說承諾(promise)正在 等待中(pending)
。如果你 console.log
一個 promise
物件,就可以驗證這點。

pending
列印 jeffBuysCake
表明承諾正在等待中。
當我們稍後一起構建 jeffBuysCake
時,你將能夠自己證明此 console.log
語句。
在與Jeff交談之後,你開始計劃下一步。你意識到如果Jeff信守諾言,並在聚會時買來一個黑森林蛋糕,你就可以按照計劃繼續派對了。
如果Jeff確實買來了蛋糕,在JavaScript中,我們說這個promise是 實現(resolved)
了。當一個承諾得到實現時,你會在 .then
呼叫中做下一件事情:
jeffBuysCake('black forest') .then(partyAsPlanned) // Woohoo! :tada::tada::tada:
如果Jeff沒給你買來蛋糕,你必須自己去麵包店買了。(該死的,Jeff!)。如果發生這種情況,我們會說承諾被 拒絕(rejected)
了。
當承諾被拒絕了,你可以在 .catch
呼叫中執行應急計劃。
jeffBuysCake('black forest') .then(partyAsPlanned) .catch(buyCakeYourself) // Grumble Grumble... #*$%
我的朋友,這就是對 Promise
的剖析了。
在JavaScript中,我們通常使用 promises
來獲取或修改一條資訊。當 promise
得到解決時,我們會對返回的資料執行某些操作。當 promise
拒絕時,我們處理錯誤:
getSomethingWithPromise() .then(data => {/* do something with data */}) .catch(err => {/* handle the error */})
現在,你知道一個 promise
如何運作了。讓我們進一步深入研究如何構建一個 promise
。
構建一個promise
你可以使用 new Promise
來建立一個promise。這個 Promise 建構函式是一個包含兩個引數 -- resolve
和 reject
的函式。
const promise = new Promise((resolve, reject) => { /* Do something here */ })
如果 resolve
被呼叫,promise成功並繼續進入 then
鏈式(操作)。你傳遞給 resolve
的引數將是接下來 then
呼叫中的引數:
const promise = new Promise((resolve, reject) => { // Note: only 1 param allowed return resolve(27) }) // Parameter passed resolve would be the arguments passed into then. promise.then(number => console.log(number)) // 27
如果 reject
被呼叫,promise失敗並繼續進入 catch
鏈式(操作)。同樣地,你傳遞給 reject
的引數將是 catch
呼叫中的引數:
const promise = new Promise((resolve, reject) => { // Note: only 1 param allowed return reject(':hankey::hankey::hankey:') }) // Parameter passed into reject would be the arguments passed into catch. promise.catch(err => console.log(err)) // :hankey::hankey::hankey:
你能看出 resolve
和 reject
都是回撥函式嗎?:wink:
讓我們練習一下,嘗試構建 jeffBuysCake
promise。
首先,你知道Jeff說他會買一個蛋糕。那就是一個承諾。所以,我們從空promise入手:
const jeffBuysCake = cakeType => { return new Promise((resolve, reject) => { // Do something here }) }
接下來,Jeff說他將在一週內購買蛋糕。讓我們使用 setTimeout
函式模擬這個等待七天的時間。我們將等待一秒,而不是七天:
const jeffBuysCake = cakeType => { return new Promise((resolve, reject) => { setTimeout(()=> { // Checks if Jeff buys a black forest cake }, 1000) }) }
如果Jeff在一秒之後買了個黑森林蛋糕,我們就會返回promise,然後將黑森林蛋糕傳遞給 then
。
如果Jeff買了另一種型別的蛋糕,我們拒接這個promise,並且說 no cake
,這會導致promise進入 catch
呼叫。
const jeffBuysCake = cakeType => { return new Promise((resolve, reject) => { setTimeout(()=> { if (cakeType - = 'black forest') { resolve('black forest cake!') } else { reject('No cake :cry:') } }, 1000) }) }
讓我們來測試下這個promise。當你在下面的 console.log
記錄時,你會看到promise正在 pedding(等待)
。(如果你立即檢查控制檯,狀態將只是暫時掛起狀態。如果你需要更多時間檢查控制檯,請隨時將超時時間延長至10秒)。
const promise = jeffBuysCake('black forest') console.log(promise)

pending
列印 jeffBuysCake
表明承諾正在等待中。
如果你在promise鏈式中新增 then
和 catch
,你會看到 black forest cake!
或 no cake :cry:
資訊,這取決於你傳入 jeffBuysCake
的蛋糕型別。
const promise = jeffBuysCake('black forest') .then(cake => console.log(cake)) .catch(nocake => console.log(nocake))

hascake
打印出來是“黑森林蛋糕”還是“沒有蛋糕”的資訊,取決於你傳入 jeffBuysCake
的(引數)。
建立一個promise不是很難,是吧?:wink:
既然你知道什麼是promise,如何製作一個promise以及如何使用promise。那麼,我們來回答 下一個問題 -- 在非同步JavaScript中為什麼要使用promise而不是回撥呢?
Promises vs Callbacks
開發人員更喜歡promises而不是callbacks有三個原因:
- Promise減少了巢狀程式碼的數量
- Promise允許你輕鬆地視覺化執行流程
- Promise讓你可以在鏈式的末尾去處理所有錯誤
為了看到這三個好處,讓我們編寫一些JavaScript程式碼,它們通過 callbacks
和 promises
來做一些非同步事情。
對於這個過程,假設你正在運營一個線上商店。你需要在客戶購買東西時向他收費,然後將他們的資訊輸入到你的資料庫中。最後,你將向他們傳送電子郵件:
- 向客戶收費
- 將客戶資訊輸入到資料庫
- 傳送電子郵件給客戶
讓我們一步一步地解決。首先,你需要一種從前端到後端獲取資訊的方法。通常,你會對這些操作使用 post
請求。
如果你使用 Express
或 Node
,則初始化程式碼可能如下所示。如果你不知道任何 Node
或 Express
(的知識點),請不要擔心。它們不是本文的主要部分。跟著下面來走:
// A little bit of NodeJS here. This is how you'll get data from the frontend through your API. app.post('/buy-thing', (req, res) => { const customer = req.body // Charge customer here })
讓我們先介紹一下基於 callback
的程式碼。在這裡,你想要向客戶收費。如果收費成功,則將其資訊新增到資料庫中。如果收費失敗,則會丟擲錯誤,因此你的伺服器可以處理錯誤。
程式碼如下所示:
// Callback based code app.post('/buy-thing', (req, res) => { const customer = req.body // First operation: charge the customer chargeCustomer(customer, (err, charge) => { if (err) throw err // Add to database here }) })
現在,讓我們切換到基於 promise
的程式碼。同樣地,你向客戶收費。如果收費成功,則通過呼叫 then
將其資訊新增到資料庫中。如果收費失敗,你將在 catch
呼叫中自動處理:
// Promised based code app.post('/buy-thing', (req, res) => { const customer = req.body // First operation: charge the customer chargeCustomer(customer) .then(/* Add to database */) .catch(err => console.log(err)) })
繼續,你可以在收費成功後將你的客戶資訊新增到資料庫中。如果資料庫操作成功,則會向客戶傳送電子郵件。否則,你會丟擲一個錯誤。
考慮到這些步驟,基於 callback
的程式碼如下:
// Callback based code app.post('/buy-thing', (req, res) => { const customer = req.body chargeCustomer(customer, (err, charge) => { if (err) throw err // Second operation: Add to database addToDatabase(customer, (err, document) => { if (err) throw err // Send email here }) }) })
對於基於 promise
的程式碼,如果資料庫操作成功,則在下一個 then
呼叫時傳送電子郵件。如果資料庫操作失敗,則會在最終的 catch
語句中自動處理錯誤:
// Promised based code app.post('/buy-thing', (req, res) => { const customer = req.body chargeCustomer(customer) // Second operation: Add to database .then(_ => addToDatabase(customer)) .then(/* Send email */) .catch(err => console.log(err)) })
繼續最後一步,在資料庫操作成功時向客戶傳送電子郵件。如果成功傳送此電子郵件,則會有成功訊息通知到你的前端。否則,你丟擲一個錯誤:
以下是基於 callback
的程式碼:
app.post('/buy-thing', (req, res) => { const customer = req.body chargeCustomer(customer, (err, charge) => { if (err) throw err addToDatabase(customer, (err, document) => { if (err) throw err sendEmail(customer, (err, result) => { if (err) throw err // Tells frontend success message. res.send('success!') }) }) }) })
然後,以下基於 promise
的程式碼:
app.post('/buy-thing', (req, res) => { const customer = req.body chargeCustomer(customer) .then(_ => addToDatabase(customer)) .then(_ => sendEmail(customer) ) .then(result => res.send('success!'))) .catch(err => console.log(err)) })
看看為什麼使用 promises
而不是 callbacks
編寫非同步程式碼要容易得多?你從回撥地獄(callback hell)一下子切換到了鏈式樂土上:joy:。
一次觸發多個promises
promises
比 callbacks
的另一個好處是,如果操作不依賴於彼此,則可以同時觸發兩個(或多個)promises,但是執行第三個操作需要兩個結果。
為此,你使用 Promise.all
方法,然後傳入一組你想要等待的promises。 then
的引數將會是一個數組,其包含你promises返回的結果。
const friesPromise = getFries() const burgerPromise = getBurger() const drinksPromise = getDrinks() const eatMeal = Promise.all([ friesPromise, burgerPromise, drinksPromise ]) .then([fries, burger, drinks] => { console.log(`Chomp. Awesome ${burger}! :hamburger:`) console.log(`Chomp. Delicious ${fries}! :fries:`) console.log(`Slurp. Ugh, shitty drink ${drink}`) })
備註:還有一個名為 Promise.race
的方法,但我還沒找到合適的用例。你可以點選這裡去檢視。
最後,我們來談談瀏覽器支援情況!如果你不能在生產環境中使用它,那為什麼要學習 promises
呢。是吧?
瀏覽器支援Promise
令人興奮的訊息是: 所有主流瀏覽器都支援promises !
如果你需要支援 IE 11
及其以下版本,你可以使用Taylor Hakes製作的Promise Polyfill。它支援IE8的promises。:open_mouth:
結語
你在本文中學到了所有關於 promises
的知識。簡而言之, promises
棒極了。它可以幫助你編寫非同步程式碼,而無需進入回撥地獄。
儘管你可能希望無論什麼時候都使用 promises
,但有些情況 callbacks
也是有意義的。不要忘記了 callbacks
啊:wink:。
如果你有疑問,請隨時在下面發表評論,我會盡快回復你的。【PS:本文譯文,若需作者解答疑問,請移步原作者文章下評論】
感謝閱讀。這篇文章是否幫助到你?如果有,我希望你考慮分享它。你可能會幫助到其他人。非常感謝!
後話
原文: https://zellwk.com/blog/js-promises/
文章首發: https://github.com/reng99/blogs/issues/19
同步掘金: https://juejin.im/post/5ccb10455188250f3a050eb5
更多內容: https://github.com/reng99/blogs
下一篇關於 async/await