JavaScript Promises:示例教程
已發表: 2022-03-11Promise 是 JavaScript 開發界的熱門話題,您絕對應該熟悉它們。 它們不容易纏住你的頭; 理解它們可能需要一些教程、示例和大量練習。
我編寫本教程的目的是幫助您理解 JavaScript Promises,並推動您更多地練習使用它們。 我將解釋什麼是 Promise,它們解決了什麼問題,以及它們是如何工作的。 本文中描述的每個步驟都附有一個jsbin
代碼示例,以幫助您繼續工作,並用作進一步探索的基礎。
什麼是 JavaScript 承諾?
Promise 是一種最終產生值的方法。 它可以被認為是 getter 函數的異步對應物。 其本質可以解釋為:
promise.then(function(value) { // Do something with the 'value' });
Promise 可以代替回調的異步使用,並且它們提供了一些優於它們的好處。 隨著越來越多的庫和框架將它們作為處理異步的主要方式,它們開始獲得支持。 Ember.js 就是這樣一個框架的一個很好的例子。
有幾個庫實現了 Promises/A+ 規範。 我們將學習基本詞彙,並通過一些 JavaScript Promise 示例以實用的方式介紹它們背後的概念。 我將在代碼示例中使用更流行的實現庫之一 rsvp.js。
準備好,我們會擲很多骰子!
獲取 rsvp.js 庫
Promise 和 rsvp.js 可以在服務器端和客戶端使用。 要為nodejs安裝它,請轉到您的項目文件夾並鍵入:
npm install --save rsvp
如果你在前端工作並使用 bower,它只是一個
bower install -S rsvp
離開。
如果你只是想在遊戲中正確,你可以通過簡單的腳本標籤包含它(使用jsbin
,你可以通過“添加庫”下拉菜單添加它):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
承諾有哪些屬性?
Promise 可以處於以下三種狀態之一: pending 、 fulfilled或denied 。 創建時,promise 處於掛起狀態。 從這裡,它可以進入已完成或已拒絕狀態。 我們稱這種轉變為承諾的解決。 Promise 的已解決狀態是它的最終狀態,因此一旦它被履行或拒絕,它就會停留在那裡。
在 rsvp.js 中創建 Promise 的方法是通過所謂的顯示構造函數。 這種類型的構造函數接受一個函數參數並立即使用兩個參數調用它, fulfill
和reject
,這可以將 promise 轉換為已fulfilled
或已rejected
狀態:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
這種 JavaScript 承諾模式被稱為揭示構造函數,因為單個函數參數向構造函數揭示了它的能力,但確保承諾的消費者不能操縱它的狀態。
Promise 的消費者可以通過then
方法添加他們的處理程序來對其狀態變化做出反應。 它需要一個實現和一個拒絕處理函數,這兩者都可能丟失。
promise.then(onFulfilled, onRejected);
根據承諾的解決過程的結果, onFulfilled
或onRejected
處理程序被異步調用。
讓我們看一個例子,它顯示了事情的執行順序:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
此代碼段打印類似於以下內容的輸出:
1 2 3 Oh, noes, threw a 4.
或者,如果我們幸運的話,我們會看到:
1 2 3 Yay, threw a 6.
這個 promises 教程演示了兩件事。
首先,我們附加到 Promise 的處理程序確實是在所有其他代碼運行後異步調用的。
其次,只有當 promise 被履行時才調用履行處理程序,並使用它解決的值(在我們的例子中,擲骰子的結果)。 拒絕處理程序也是如此。
鏈接承諾並向下滲透
規範要求then
函數(處理程序)也必須返回一個 Promise,這可以將 Promise 鏈接在一起,從而使代碼看起來幾乎是同步的:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
在這裡, signupPayingUser
返回一個承諾,一旦完成,承諾鏈中的每個函數都會被調用,並使用前一個處理程序的返回值。 出於所有實際目的,這會在不阻塞主執行線程的情況下序列化調用。
為了查看每個承諾如何通過鏈中前一項的返回值得到解決,我們回到擲骰子。 我們最多要擲骰子 3 次,或者直到前 6 個出現 jsbin:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
當你運行這個 Promise 示例代碼時,你會在控制台上看到類似這樣的內容:
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
當 toss 不是 6 時, tossASix
返回的 Promise 被拒絕,因此拒絕處理程序會使用實際的 toss 來調用。 logAndTossAgain
在控制台上打印結果並返回代表另一次擲骰子的承諾。 反過來,這種折騰也會被下一個logAndTossAgain
拒絕並註銷。

但是,有時您會很幸運*,並設法擲出六:
Tossed a 4, need to try again. Yay, managed to toss a 6.
* 你不必那麼幸運。 如果您擲三個骰子,則有約 42% 的機會擲出至少一個六。
這個例子也教會了我們更多的東西。 看看在第一次成功擲出 6 後,如何不再投擲? 請注意,鏈中的所有履行處理程序(對then
的調用中的第一個參數)都是null
,除了最後一個logSuccess
。 規範要求如果處理程序(履行或拒絕)不是函數,則返回的承諾必須以相同的值解決(履行或拒絕)。 在上面的 Promise 示例中,執行處理程序null
不是一個函數,並且 Promise 的值是用 6 完成的。所以then
調用(鏈中的下一個)返回的 Promise 也將被執行以 6 作為其值。
這一直重複,直到存在一個實際的履行處理程序(一個函數),所以履行會逐漸下降,直到它被處理。 在我們的例子中,這發生在鏈的末端,它被愉快地註銷到控制台。
處理錯誤
Promises/A+ 規範要求,如果一個 Promise 被拒絕或在拒絕處理程序中拋出錯誤,它應該由源“下游”的拒絕處理程序處理。
利用下面的涓滴技術提供了一種處理錯誤的干淨方法:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
因為拒絕處理程序只添加在鏈的最末端,所以如果鏈中的任何履行處理程序被拒絕或拋出錯誤,它會慢慢下降,直到遇到displayAndSendErrorReport
。
讓我們回到我們心愛的骰子,看看它的實際效果。 假設我們只想異步擲骰子並打印出結果:
var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
當你運行它時,什麼也沒有發生。 控制台上沒有打印任何內容,似乎也沒有拋出任何錯誤。
實際上,確實會拋出錯誤,我們只是看不到它,因為鏈中沒有拒絕處理程序。 由於處理程序中的代碼使用新堆棧異步執行,因此它甚至不會被註銷到控制台。 讓我們解決這個問題:
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
現在運行上面的代碼確實會顯示錯誤:
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
我們忘記從logAndTossAgain
返回一些東西,第二個承諾用undefined
實現。 然後,下一個履行處理程序在嘗試調用toUpperCase
時崩潰了。 這是要記住的另一件重要的事情:總是從處理程序返回一些東西,或者在後續處理程序中準備好什麼都沒有傳遞。
建得更高
我們現在已經在本教程的示例代碼中看到了 JavaScript Promise 的基礎知識。 使用它們的一個很大的好處是,它們可以以簡單的方式組合,以產生具有我們想要的行為的“複合”promise。 rsvp.js
庫提供了其中的一些,您始終可以使用原語和這些更高級別的原語創建自己的。
對於最後一個最複雜的示例,我們將前往 AD&D 角色扮演的世界並擲骰子來獲得角色分數。 這些分數是通過為角色的每個技能擲三個骰子來獲得的。
讓我先在這裡粘貼代碼,然後解釋什麼是新的:
function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
我們從上一個代碼示例中熟悉了toss
。 它只是創建了一個承諾,該承諾總是通過擲骰子的結果來實現。 我使用了RSVP.resolve
,這是一種方便的方法,可以用更少的儀式來創建這樣的承諾(參見上面代碼中的 [1])。
在threeDice
中,我創建了3 個promise,每個promise 代表一次擲骰子,最後將它們與RSVP.all
結合起來。 RSVP.all
接受一組承諾,並使用一組已解決的值來解決,每個組成承諾一個,同時保持它們的順序。 這意味著我們在 results 中得到了折騰的results
(參見上面代碼中的 [2]),並且我們返回了一個用它們的總和來實現的 promise(參見上面代碼中的 [3])。
解析得到的 promise 然後記錄總數:
"Rolled 11 with three dice"
使用 Promise 解決實際問題
JavaScript Promise 用於解決應用程序中的問題,這些問題遠比異步擲骰子復雜得多。
如果您將擲三個骰子替換為向單獨的端點發送三個 ajax 請求並在所有端點都成功返回(或者如果其中任何一個都失敗)時繼續,那麼您已經有了一個有用的 promise 和RSVP.all
應用程序。
如果使用得當,Promise 會生成易於理解的代碼,因此比回調更容易調試。 無需設置有關例如錯誤處理的約定,因為它們已經是規範的一部分。
在本 JavaScript 教程中,我們幾乎沒有觸及到 Promise 可以做什麼的皮毛。 Promise 庫提供了許多可供您使用的方法和低級構造函數。 掌握這些,天空就是你可以用它們做的極限。
關於作者
Balint Erdi 很久以前是一個偉大的角色扮演和 AD&D 粉絲,現在是一個偉大的承諾和 Ember.js 粉絲。 一直不變的是他對搖滾樂的熱情。 這就是為什麼他決定寫一本關於 Ember.js 的書,以搖滾樂作為書中應用程序的主題。 在此處註冊以了解它何時啟動。