你有沒有在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
物件,就可以驗證這點。
列印
jeffBuysCake
表明承諾正在等待中。
當我們稍後一起構建
jeffBuysCake
時,你將能夠自己證明此console.log
語句。
在與Jeff交談之後,你開始計劃下一步。你意識到如果Jeff信守諾言,並在聚會時買來一個黑森林蛋糕,你就可以按照計劃繼續派對了。
如果Jeff確實買來了蛋糕,在JavaScript中,我們說這個promise是實現(resolved)
了。當一個承諾得到實現時,你會在.then
呼叫中做下一件事情:
jeffBuysCake('black forest')
.then(partyAsPlanned) // Woohoo! ???
複製程式碼
如果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('???')
})
// Parameter passed into reject would be the arguments passed into catch.
promise.catch(err => console.log(err)) // ???
複製程式碼
你能看出
resolve
和reject
都是回撥函式嗎??
讓我們練習一下,嘗試構建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 ?')
}
}, 1000)
})
}
複製程式碼
讓我們來測試下這個promise。當你在下面的console.log
記錄時,你會看到promise正在pedding(等待)
。(如果你立即檢查控制檯,狀態將只是暫時掛起狀態。如果你需要更多時間檢查控制檯,請隨時將超時時間延長至10秒)。
const promise = jeffBuysCake('black forest')
console.log(promise)
複製程式碼
列印
jeffBuysCake
表明承諾正在等待中。
如果你在promise鏈式中新增then
和catch
,你會看到black forest cake!
或no cake ?
資訊,這取決於你傳入jeffBuysCake
的蛋糕型別。
const promise = jeffBuysCake('black forest')
.then(cake => console.log(cake))
.catch(nocake => console.log(nocake))
複製程式碼
列印出來是“黑森林蛋糕”還是“沒有蛋糕”的資訊,取決於你傳入
jeffBuysCake
的(引數)。
建立一個promise不是很難,是吧??
既然你知道什麼是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)一下子切換到了鏈式樂土上?。
一次觸發多個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}! ?`)
console.log(`Chomp. Delicious ${fries}! ?`)
console.log(`Slurp. Ugh, shitty drink ${drink} ? `)
})
複製程式碼
備註:還有一個名為
Promise.race
的方法,但我還沒找到合適的用例。你可以點選這裡去檢視。
最後,我們來談談瀏覽器支援情況!如果你不能在生產環境中使用它,那為什麼要學習promises
呢。是吧?
瀏覽器支援Promise
令人興奮的訊息是:所有主流瀏覽器都支援promises!
如果你需要支援IE 11
及其以下版本,你可以使用Taylor Hakes製作的Promise Polyfill。它支援IE8的promises。?
結語
你在本文中學到了所有關於promises
的知識。簡而言之,promises
棒極了。它可以幫助你編寫非同步程式碼,而無需進入回撥地獄。
儘管你可能希望無論什麼時候都使用promises
,但有些情況callbacks
也是有意義的。不要忘記了callbacks
啊?。
如果你有疑問,請隨時在下面發表評論,我會盡快回復你的。【PS:本文譯文,若需作者解答疑問,請移步原作者文章下評論】
感謝閱讀。這篇文章是否幫助到你?如果有,我希望你考慮分享它。你可能會幫助到其他人。非常感謝!
後話
下一篇關於 async/await