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