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