想象壹下,妳是壹位頂尖歌手,粉絲沒日沒夜地詢問妳下首歌什麽時候發。
爲了從中解放,妳承諾(promise)會在單曲發布的第壹時間發給他們。妳給了粉絲們壹個列表。他們可以在上面填寫他們的電子郵件地址,以便當歌曲發布後,讓所有訂閱了的人能夠立即收到。即便遇到不測,例如錄音室發生了火災,以致妳無法發布新歌,他們也能及時收到相關通知。
每個人都很開心:妳不會被任何人催促,粉絲們也不用擔心錯過歌曲發行。
這是我們在編程中經常遇到的事兒與真實生活的類比:
“生産者代碼(producing code)”會做壹些事兒,並且會需要壹些時間。例如,通過網絡加載數據的代碼。它就像壹位“歌手”。
“消費者代碼(consuming code)”想要在“生産者代碼”完成工作的第壹時間就能獲得其工作成果。許多函數可能都需要這個結果。這些就是“粉絲”。
Promise 是將“生産者代碼”和“消費者代碼”連接在壹起的壹個特殊的 JavaScript 對象。用我們的類比來說:這就是就像是“訂閱列表”。“生産者代碼”花費它所需的任意長度時間來産出所承諾的結果,而 “promise” 將在它(譯注:指的是“生産者代碼”,也就是下文所說的 executor)准備好時,將結果向所有訂閱了的代碼開放。
這種類比並不十分准確,因爲 JavaScript 的 promise 比簡單的訂閱列表更加複雜:它們還擁有其他的功能和局限性。但以此開始挺好的。
Promise 對象的構造器(constructor)語法如下:
let promise = new Promise(function(resolve, reject) { // executor(生産者代碼,“歌手”) });
傳遞給 new Promise
的函數被稱爲 executor。當 new Promise
被創建,executor 會自動運行。它包含最終應産出結果的生産者代碼。按照上面的類比:executor 就是“歌手”。
它的參數 resolve
和 reject
是由 JavaScript 自身提供的回調。我們的代碼僅在 executor 的內部。
當 executor 獲得了結果,無論是早還是晚都沒關系,它應該調用以下回調之壹:
resolve(value)
—— 如果任務成功完成並帶有結果 value
。
reject(error)
—— 如果出現了 error,error
即爲 error 對象。
所以總結壹下就是:executor 會自動運行並嘗試執行壹項工作。嘗試結束後,如果成功則調用 resolve
,如果出現 error 則調用 reject
。
由 new Promise
構造器返回的 promise
對象具有以下內部屬性:
state
—— 最初是 "pending"
,然後在 resolve
被調用時變爲 "fulfilled"
,或者在 reject
被調用時變爲 "rejected"
。
result
—— 最初是 undefined
,然後在 resolve(value)
被調用時變爲 value
,或者在 reject(error)
被調用時變爲 error
。
所以,executor 最終將 promise
移至以下狀態之壹:
稍後我們將看到“粉絲”如何訂閱這些更改。
下面是壹個 promise 構造器和壹個簡單的 executor 函數,該 executor 函數具有包含時間(即 setTimeout
)的“生産者代碼”:
let promise = new Promise(function(resolve, reject) { // 當 promise 被構造完成時,自動執行此函數 // 1 秒後發出工作已經被完成的信號,並帶有結果 "done" setTimeout(() => resolve("done"), 1000); });
通過運行上面的代碼,我們可以看到兩件事兒:
executor 被自動且立即調用(通過 new Promise
)。
executor 接受兩個參數:resolve
和 reject
。這些函數由 JavaScript 引擎預先定義,因此我們不需要創建它們。我們只需要在准備好(譯注:指的是 executor 准備好)時調用其中之壹即可。
經過 1 秒的“處理”後,executor 調用 resolve("done")
來産生結果。這將改變 promise
對象的狀態:
這是壹個成功完成任務的例子,壹個“成功實現了的諾言”。
下面則是壹個 executor 以 error 拒絕 promise 的示例:
let promise = new Promise(function(resolve, reject) { // 1 秒後發出工作已經被完成的信號,並帶有 error setTimeout(() => reject(new Error("Whoops!")), 1000); });
對 reject(...)
的調用將 promise 對象的狀態移至 "rejected"
:
總而言之,executor 應該執行壹項工作(通常是需要花費壹些時間的事兒),然後調用 resolve
或 reject
來改變對應的 promise 對象的狀態。
與最初的 “pending” promise 相反,壹個 resolved 或 rejected 的 promise 都會被稱爲 “settled”。
只有壹個結果或壹個 error
executor 只能調用壹個 resolve
或壹個 reject
。任何狀態的更改都是最終的。
所有其他的再對 resolve
和 reject
的調用都會被忽略:
let promise = new Promise(function(resolve, reject) { resolve("done"); reject(new Error("…")); // 被忽略 setTimeout(() => resolve("…")); // 被忽略 });
關鍵就在于,壹個由 executor 完成的工作只能有壹個結果或壹個 error。
並且,resolve/reject
只需要壹個參數(或不包含任何參數),並且將忽略額外的參數。
以 Error
對象 reject
如果什麽東西出了問題,executor 應該調用 reject
。這可以使用任何類型的參數來完成(就像 resolve
壹樣)。但建議使用 Error
對象(或繼承自 Error
的對象)。這樣做的理由很快就會顯而易見。
resolve/reject 可以立即進行
實際上,executor 通常是異步執行某些操作,並在壹段時間後調用 resolve/reject
,但這不是必須的。我們還可以立即調用 resolve
或 reject
,就像這樣:
let promise = new Promise(function(resolve, reject) { // 不花時間去做這項工作 resolve(123); // 立即給出結果:123 });
例如,當我們開始做壹個任務,隨後發現壹切都已經完成並已被緩存時,可能就會發生這種情況。
這挺好。我們立即就有了壹個 resolved 的 promise。
state
和 result
都是內部的
Promise 對象的 state
和 result
屬性都是內部的。我們無法直接訪問它們。但我們可以對它們使用 .then
/.catch
/.finally
方法。我們在下面對這些方法進行了描述。
Promise 對象充當的是 executor(“生産者代碼”或“歌手”)和消費函數(“粉絲”)之間的連接,後者將接收結果或 error。可以通過使用 .then
和 .catch
方法注冊消費函數。
最重要最基礎的壹個就是 .then
。
語法如下:
promise.then( function(result) { /* handle a successful result */ }, function(error) { /* handle an error */ } );
.then
的第壹個參數是壹個函數,該函數將在 promise resolved 且接收到結果後執行。
.then
的第二個參數也是壹個函數,該函數將在 promise rejected 且接收到 error 信息後執行。
例如,以下是對成功 resolved 的 promise 做出的反應:
let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve("done!"), 1000); }); // resolve 運行 .then 中的第壹個函數 promise.then( result => alert(result), // 1 秒後顯示 "done!" error => alert(error) // 不運行 );
第壹個函數被運行了。
在 reject 的情況下,運行第二個:
let promise = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // reject 運行 .then 中的第二個函數 promise.then( result => alert(result), // 不運行 error => alert(error) // 1 秒後顯示 "Error: Whoops!" );
如果我們只對成功完成的情況感興趣,那麽我們可以只爲 .then
提供壹個函數參數:
let promise = new Promise(resolve => { setTimeout(() => resolve("done!"), 1000); }); promise.then(alert); // 1 秒後顯示 "done!"
如果我們只對 error 感興趣,那麽我們可以使用 null
作爲第壹個參數:.then(null, errorHandlingFunction)
。或者我們也可以使用 .catch(errorHandlingFunction)
,其實是壹樣的:
let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // .catch(f) 與 promise.then(null, f) 壹樣 promise.catch(alert); // 1 秒後顯示 "Error: Whoops!"
.catch(f)
調用是 .then(null, f)
的完全的模擬,它只是壹個簡寫形式。
就像常規 try {...} catch {...}
中的 finally
子句壹樣,promise 中也有 finally
。
調用 .finally(f)
類似于 .then(f, f)
,因爲當 promise settled 時 f
就會執行:無論 promise 被 resolve 還是 reject。
finally
的功能是設置壹個處理程序在前面的操作完成後,執行清理/終結。
例如,停止加載指示器,關閉不再需要的連接等。
把它想象成派對的終結者。無論派對是好是壞,有多少朋友參加,我們都需要(或者至少應該)在它之後進行清理。
代碼可能看起來像這樣:
new Promise((resolve, reject) => { /* 做壹些需要時間的事,之後調用可能會 resolve 也可能會 reject */ }) // 在 promise 爲 settled 時運行,無論成功與否 .finally(() => stop loading indicator) // 所以,加載指示器(loading indicator)始終會在我們繼續之前停止 .then(result => show result, err => show error)
請注意,finally(f)
並不完全是 then(f,f)
的別名。
它們之間有重要的區別:
finally
處理程序(handler)沒有參數。在 finally
中,我們不知道 promise 是否成功。沒關系,因爲我們的任務通常是執行“常規”的完成程序(finalizing procedures)。
請看上面的例子:如妳所見,finally
處理程序沒有參數,promise 的結果由下壹個處理程序處理。
finally
處理程序將結果或 error “傳遞”給下壹個合適的處理程序。
例如,在這結果被從 finally
傳遞給了 then
:
new Promise((resolve, reject) => { setTimeout(() => resolve("value"), 2000) }) .finally(() => alert("Promise ready")) // 先觸發 .then(result => alert(result)); // <-- .then 顯示 "value"
正如我們所看到的,第壹個 promise 返回的 value
通過 finally
被傳遞給了下壹個 then
。
這非常方便,因爲 finally
並不意味著處理壹個 promise 的結果。如前所述,無論結果是什麽,它都是進行常規清理的地方。
下面是壹個 promise 返回結果爲 error 的示例,讓我們看看它是如何通過 finally
被傳遞給 catch
的:
new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) // 先觸發 .catch(err => alert(err)); // <-- .catch 顯示這個 error
finally
處理程序也不應該返回任何內容。如果它返回了,返回的值會默認被忽略。
此規則的唯壹例外是當 finally
處理程序抛出 error 時。此時這個 error(而不是任何之前的結果)會被轉到下壹個處理程序。
總結:
finally
處理程序沒有得到前壹個處理程序的結果(它沒有參數)。而這個結果被傳遞給了下壹個合適的處理程序。
如果 finally
處理程序返回了壹些內容,那麽這些內容會被忽略。
當 finally
抛出 error 時,執行將轉到最近的 error 的處理程序。
如果我們正確使用 finally
(將其用于常規清理),那麽這些功能將很有用。
我們可以對 settled 的 promise 附加處理程序
如果 promise 爲 pending 狀態,.then/catch/finally
處理程序(handler)將等待它的結果。
有時候,當我們向壹個 promise 添加處理程序時,它可能已經 settled 了。
在這種情況下,這些處理程序會立即執行:
// 下面這 promise 在被創建後立即變爲 resolved 狀態 let promise = new Promise(resolve => resolve("done!")); promise.then(alert); // done!(立刻顯示)
請注意這使得 promise 比現實生活中的“訂閱列表”方案強大得多。如果歌手已經發布了他們的單曲,然後某個人在訂閱列表上進行了注冊,則他們很可能不會收到該單曲。實際生活中的訂閱必須在活動開始之前進行。
Promise 則更加靈活。我們可以隨時添加處理程序(handler):如果結果已經在了,它們就會執行。
接下來,讓我們看壹下關于 promise 如何幫助我們編寫異步代碼的更多實際示例。
我們從上壹章獲得了用于加載腳本的 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); }
讓我們用 promise 重寫它。
新函數 loadScript
將不需要回調。取而代之的是,它將創建並返回壹個在加載完成時 resolve 的 promise 對象。外部代碼可以使用 .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...'));
我們立刻就能發現 promise 相較于基于回調的模式的壹些好處:
promise | callback |
---|---|
promise 允許我們按照自然順序進行編碼。首先,我們運行 loadScript ,之後,用 .then 來處理結果。 | 在調用 loadScript(script, callback) 時,我們必須有壹個 callback 函數可供使用。換句話說,在調用 loadScript 之前,我們必須知道如何處理結果。 |
我們可以根據需要,在 promise 上多次調用 .then 。每次調用,我們都會在“訂閱列表”中添加壹個新的“粉絲”,壹個新的訂閱函數。在下壹章將對此內容進行詳細介紹:Promise 鏈。 | 只能有壹個回調。 |
因此,promise 爲我們提供了更好的代碼流和靈活性。但其實還有更多相關內容。我們將在下壹章看到。
下列這段代碼會輸出什麽?
let promise = new Promise(function(resolve, reject) { resolve(1); setTimeout(() => resolve(2), 1000); }); promise.then(alert);
輸出爲:1
。
第二個對 resolve
的調用會被忽略,因爲只有第壹次對 reject/resolve
的調用才會被處理。進壹步的調用都會被忽略。
內建函數 setTimeout
使用了回調函數。請創建壹個基于 promise 的替代方案。
函數 delay(ms)
應該返回壹個 promise。這個 promise 應該在 ms
毫秒後被 resolve,所以我們可以向其中添加 .then
,像這樣:
function delay(ms) { // 妳的代碼 } delay(3000).then(() => alert('runs after 3 seconds'));
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } delay(3000).then(() => alert('runs after 3 seconds'));
請注意,在此任務中 resolve
是不帶參數調用的。我們不從 delay
中返回任何值,只是確保延遲即可。
重寫任務 帶回調的圓圈動畫 的解決方案中的 showCircle
函數,以使其返回壹個 promise,而不接受回調。
新的用法:
showCircle(150, 150, 100).then(div => { div.classList.add('message-ball'); div.append("Hello, world!"); });
以任務 帶回調的圓圈動畫 的解決方案爲基礎。
使用沙箱打開解決方案。