想像一下,你是位頂尖歌手,粉絲們日夜詢問你即將推出的歌曲。
為了喘口氣,你承諾在歌曲發行時寄給他們。你給粉絲們一份名單。他們可以填寫他們的電子郵件地址,這樣一來,當歌曲上架時,所有訂閱者都能立即收到。即使發生了非常糟糕的事,比方說錄音室發生火災,導致你無法發行歌曲,他們還是會收到通知。
每個人都開心:你,因為人們不再擠你了,粉絲,因為他們不會錯過這首歌。
這是我們在程式設計中經常遇到的情況的真實類比
- 一個「產生程式碼」,它會做一些事情並花費時間。例如,一些透過網路載入資料的程式碼。那是一個「歌手」。
- 一個「消耗程式碼」,它想要在「產生程式碼」準備好後得到結果。許多函式可能需要那個結果。這些是「粉絲」。
- 一個承諾是一個特殊的 JavaScript 物件,它將「產生程式碼」和「消耗程式碼」連結在一起。根據我們的類比:這是「訂閱清單」。「產生程式碼」花費任何它需要花費的時間來產生承諾的結果,而「承諾」在準備好後讓所有訂閱的程式碼可以使用那個結果。
這個類比並不完全準確,因為 JavaScript 承諾比一個簡單的訂閱清單更複雜:它們有額外的功能和限制。但這是一個好的開始。
一個承諾物件的建構函式語法是
let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});
傳遞給 new Promise
的函式稱為執行器。當 new Promise
被建立時,執行器會自動執行。它包含應該最終產生結果的產生程式碼。根據上面的類比:執行器是「歌手」。
它的引數 resolve
和 reject
是由 JavaScript 本身提供的回呼函式。我們的程式碼只在執行器內部。
當執行器取得結果時,無論早或晚,它都應該呼叫其中一個回呼函式
resolve(value)
— 如果工作已成功完成,結果為value
。reject(error)
— 如果發生錯誤,error
是錯誤物件。
因此,總之:執行器自動執行並嘗試執行一個工作。當它完成嘗試時,如果成功,它會呼叫 resolve
,如果發生錯誤,它會呼叫 reject
。
由 new Promise
建構函式回傳的 promise
物件有這些內部屬性
state
— 最初為"pending"
,然後在呼叫resolve
時變更為"fulfilled"
,或在呼叫reject
時變更為"rejected"
。result
— 最初為undefined
,然後在呼叫resolve(value)
時變更為value
,或在呼叫reject(error)
時變更為error
。
因此,執行器最終將 promise
移至其中一個狀態
稍後我們將看到「粉絲」如何訂閱這些變更。
以下是 Promise 建構函式和一個簡單執行函式的範例,其中包含需要花費時間的「產生程式碼」(透過 setTimeout
)
let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});
執行上述程式碼,我們可以看到兩件事
-
執行函式會自動且立即被呼叫(透過
new Promise
)。 -
執行函式會收到兩個引數:
resolve
和reject
。這些函式是由 JavaScript 引擎預先定義的,因此我們不需要建立它們。我們只應在準備好時呼叫其中一個函式。經過一秒鐘的「處理」後,執行函式會呼叫
resolve("done")
來產生結果。這會變更promise
物件的狀態
這是成功完成工作的一個範例,也就是「已完成的 Promise」。
以下是一個執行函式拒絕 Promise 並傳回錯誤的範例
let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
呼叫 reject(...)
會將 Promise 物件移至「已拒絕」狀態
總之,執行函式應執行工作(通常是需要花費時間的工作),然後呼叫 resolve
或 reject
來變更對應 Promise 物件的狀態。
已解決或已拒絕的 Promise 稱為「已解決」,與最初「待處理」的 Promise 相反。
執行函式應只呼叫一個 resolve
或一個 reject
。任何狀態變更都是最終的。
所有後續的 resolve
和 reject
呼叫都會被忽略
let promise = new Promise(function(resolve, reject) {
resolve("done");
reject(new Error("…")); // ignored
setTimeout(() => resolve("…")); // ignored
});
這個概念是執行函式所做的工作只能有一個結果或一個錯誤。
此外,resolve
/reject
只會預期一個引數(或沒有),並會忽略其他引數。
Error
物件拒絕如果發生錯誤,執行函式應呼叫 reject
。這可以使用任何類型的引數來完成(就像 resolve
一樣)。但建議使用 Error
物件(或繼承自 Error
的物件)。這樣做的原因將很快變得明顯。
resolve
/reject
在實務上,執行函式通常會非同步執行某些動作,並在一段時間後呼叫 resolve
/reject
,但它不必這麼做。我們也可以立即呼叫 resolve
或 reject
,如下所示
let promise = new Promise(function(resolve, reject) {
// not taking our time to do the job
resolve(123); // immediately give the result: 123
});
例如,這可能會發生在我們開始執行工作時,但隨後發現所有工作都已完成並快取。
這很好。我們立即有一個已解決的 Promise。
state
和 result
是內部的Promise 物件的 state
和 result
屬性是內部的。我們無法直接存取它們。我們可以使用 .then
/.catch
/.finally
方法來存取。它們的說明如下。
使用者:then、catch
Promise 物件作為執行器(「產生程式碼」或「歌手」)和使用函式(「粉絲」)之間的連結,使用函式會接收結果或錯誤。可以使用 .then
和 .catch
方法來註冊(訂閱)使用函式。
then
最重要的基本函式是 .then
。
語法是
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
.then
的第一個引數是承諾已解決並接收結果時執行的函式。
.then
的第二個引數是承諾被拒絕並接收錯誤時執行的函式。
例如,以下是對已成功解決承諾的反應
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve runs the first function in .then
promise.then(
result => alert(result), // shows "done!" after 1 second
error => alert(error) // doesn't run
);
第一個函式已執行。
而在被拒絕的情況下,第二個函式已執行
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// reject runs the second function in .then
promise.then(
result => alert(result), // doesn't run
error => alert(error) // shows "Error: Whoops!" after 1 second
);
如果我們只對成功完成有興趣,則我們可以只提供一個函式引數給 .then
let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // shows "done!" after 1 second
catch
如果我們只對錯誤有興趣,則我們可以使用 null
作為第一個引數:.then(null, errorHandlingFunction)
。或者我們可以使用 .catch(errorHandlingFunction)
,它完全相同
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second
呼叫 .catch(f)
是 .then(null, f)
的完整類比,它只是一個簡寫。
清理:finally
就像在常規 try {...} catch {...}
中有一個 finally
子句一樣,承諾中也有 finally
。
呼叫 .finally(f)
類似於 .then(f, f)
,因為 f
總是在承諾解決時執行:無論是解決還是拒絕。
finally
的想法是設定一個處理常式,以便在前一個操作完成後執行清理/完成。
例如,停止載入指標、關閉不再需要的連線等。
把它想像成一個派對結束者。無論派對好壞,有多少朋友參加,我們仍然需要(或至少應該)在派對結束後進行清理。
程式碼可能如下所示
new Promise((resolve, reject) => {
/* do something that takes time, and then call resolve or maybe reject */
})
// runs when the promise is settled, doesn't matter successfully or not
.finally(() => stop loading indicator)
// so the loading indicator is always stopped before we go on
.then(result => show result, err => show error)
請注意,finally(f)
並不完全是 then(f,f)
的別名。
有重要的差異
-
finally
處理常式沒有引數。在finally
中,我們不知道承諾是否成功。這沒關係,因為我們的任務通常是執行「一般」完成程序。請看上面的範例:正如您所見,
finally
處理常式沒有引數,而承諾結果是由下一個處理常式處理的。 -
一個
finally
處理器會「傳遞」結果或錯誤至下一個合適的處理器。例如,這裡的結果會透過
finally
傳遞至then
new Promise((resolve, reject) => { setTimeout(() => resolve("value"), 2000); }) .finally(() => alert("Promise ready")) // triggers first .then(result => alert(result)); // <-- .then shows "value"
如你所見,第一個承諾回傳的
value
會透過finally
傳遞至下一個then
。這非常方便,因為
finally
並非用於處理承諾結果。如前所述,它是一個執行一般清理的地方,不論結果為何。這裡有一個錯誤範例,讓我們看看它是如何透過
finally
傳遞至catch
new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) // triggers first .catch(err => alert(err)); // <-- .catch shows the error
-
一個
finally
處理器也不應該回傳任何東西。如果回傳,回傳值會被靜默忽略。這個規則唯一的例外是當一個
finally
處理器擲出錯誤時。此時這個錯誤會傳遞至下一個處理器,而非任何先前的結果。
總結
- 一個
finally
處理器不會取得前一個處理器的結果(它沒有參數)。這個結果會被傳遞至下一個合適的處理器。 - 如果一個
finally
處理器回傳某個東西,它會被忽略。 - 當
finally
擲出錯誤時,執行會傳遞至最近的錯誤處理器。
這些功能非常有用,如果我們正確使用 finally
,它們會讓事情以正確的方式運作:用於一般清理程序。
如果一個承諾正在處理中,.then/catch/finally
處理器會等待它的結果。
有時,當我們將一個處理器新增至一個承諾時,它可能已經解決了。
在這種情況下,這些處理器會立即執行
// the promise becomes resolved immediately upon creation
let promise = new Promise(resolve => resolve("done!"));
promise.then(alert); // done! (shows up right now)
請注意,這使得承諾比實際生活中的「訂閱清單」場景更強大。如果歌手已經發布了他們的歌曲,然後有人在訂閱清單上註冊,他們可能不會收到那首歌。實際生活中的訂閱必須在事件發生之前完成。
承諾更靈活。我們可以隨時新增處理器:如果結果已經存在,它們就會執行。
範例:loadScript
接下來,讓我們看看承諾如何幫助我們撰寫非同步程式碼的更實際範例。
我們有來自前一章節的 loadScript
函式,用於載入腳本。
以下是基於回呼的變體,只是為了提醒我們
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
讓我們使用承諾重新撰寫它。
新的函式 loadScript
將不需要回呼。相反地,它會建立並回傳一個承諾物件,在載入完成時解析。外部程式碼可以使用 .then
新增處理器(訂閱函式)至它
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
用法
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
我們可以立即看到它比基於回呼的模式有一些好處
承諾 | 回呼 |
---|---|
Promises 讓我們可以按自然順序做事。首先,我們執行 loadScript(script) ,然後 .then 我們撰寫如何處理結果。 |
呼叫 loadScript(script, callback) 時,我們必須準備好一個 callback 函式。換句話說,我們必須在呼叫 loadScript 之前 知道如何處理結果。 |
我們可以對 Promise 呼叫 .then 任意次數。每次我們都在「訂閱清單」中新增一個新的「粉絲」,也就是一個新的訂閱函式。下一章會進一步說明:Promises 串接。 |
只能有一個 callback。 |
因此,promises 讓我們有更好的程式碼流程和彈性。但還有更多。我們會在下一章看到。
留言
<code>
標籤,若要插入多行程式碼,請將它們包覆在<pre>
標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)