W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
我們回顧一下 簡(jiǎn)介:回調(diào) 一章中提到的問(wèn)題:我們有一系列的異步任務(wù)要一個(gè)接一個(gè)地執(zhí)行 —— 例如,加載腳本。我們?nèi)绾螌?xiě)出更好的代碼呢?
Promise 提供了一些方案來(lái)做到這一點(diǎn)。
在本章中,我們將一起學(xué)習(xí) promise 鏈。
它看起來(lái)就像這樣:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
它的想法是通過(guò) .then
處理程序(handler)鏈進(jìn)行傳遞 result。
運(yùn)行流程如下:
(*)
?,.then
? 處理程序被調(diào)用 ?(**)
?,它又創(chuàng)建了一個(gè)新的 promise(以 ?2
? 作為值 resolve)。then
? ?(***)
? 得到了前一個(gè) then 的值,對(duì)該值進(jìn)行處理(*2)并將其傳遞給下一個(gè)處理程序。隨著 result 在處理程序鏈中傳遞,我們可以看到一系列的 alert
調(diào)用:1
→ 2
→ 4
。
這樣之所以是可行的,是因?yàn)槊總€(gè)對(duì) .then
的調(diào)用都會(huì)返回了一個(gè)新的 promise,因此我們可以在其之上調(diào)用下一個(gè) .then
。
當(dāng)處理程序返回一個(gè)值時(shí),它將成為該 promise 的 result,所以將使用它調(diào)用下一個(gè) .then
。
新手常犯的一個(gè)經(jīng)典錯(cuò)誤:從技術(shù)上講,我們也可以將多個(gè) .then
添加到一個(gè) promise 上。但這并不是 promise 鏈(chaining)。
例如:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
我們?cè)谶@里所做的只是一個(gè) promise 的幾個(gè)處理程序。它們不會(huì)相互傳遞 result;相反,它們之間彼此獨(dú)立運(yùn)行處理任務(wù)。
這是它的一張示意圖(你可以將其與上面的鏈?zhǔn)秸{(diào)用做一下比較):
在同一個(gè) promise 上的所有 .then
獲得的結(jié)果都相同 —— 該 promise 的結(jié)果。所以,在上面的代碼中,所有 alert
都顯示相同的內(nèi)容:1
。
實(shí)際上我們極少遇到一個(gè) promise 需要多個(gè)處理程序的情況。使用鏈?zhǔn)秸{(diào)用的頻率更高。
.then(handler)
中所使用的處理程序(handler)可以創(chuàng)建并返回一個(gè) promise。
在這種情況下,其他的處理程序?qū)⒌却?settled 后再獲得其結(jié)果。
例如:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
這里第一個(gè) .then
顯示 1
并在 (*)
行返回 new Promise(…)
。1 秒后它會(huì)進(jìn)行 resolve,然后 result(resolve
的參數(shù),在這里它是 result*2
)被傳遞給第二個(gè) .then
的處理程序。這個(gè)處理程序位于 (**)
行,它顯示 2
,并執(zhí)行相同的行為。
所以輸出與前面的示例相同:1 → 2 → 4,但是現(xiàn)在在每次 alert
調(diào)用之間會(huì)有 1 秒鐘的延遲。
返回 promise 使我們能夠構(gòu)建異步行為鏈。
讓我們將本章所講的這個(gè)特性與在 上一章 中定義的 promise 化的 loadScript
結(jié)合使用,按順序依次加載腳本:
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// 使用在腳本中聲明的函數(shù)
// 以證明腳本確實(shí)被加載完成了
one();
two();
three();
});
我們可以用箭頭函數(shù)來(lái)重寫(xiě)代碼,讓其變得簡(jiǎn)短一些:
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// 腳本加載完成,我們可以在這兒使用腳本中聲明的函數(shù)
one();
two();
three();
});
在這兒,每個(gè) loadScript
調(diào)用都返回一個(gè) promise,并且在它 resolve 時(shí)下一個(gè) .then
開(kāi)始運(yùn)行。然后,它啟動(dòng)下一個(gè)腳本的加載。所以,腳本是一個(gè)接一個(gè)地加載的。
我們可以向鏈中添加更多的異步行為。請(qǐng)注意,代碼仍然是“扁平”的 —— 它向下增長(zhǎng),而不是向右。這里沒(méi)有“厄運(yùn)金字塔”的跡象。
從技術(shù)上講,我們可以向每個(gè) loadScript
直接添加 .then
,就像這樣:
loadScript("/article/promise-chaining/one.js").then(script1 => {
loadScript("/article/promise-chaining/two.js").then(script2 => {
loadScript("/article/promise-chaining/three.js").then(script3 => {
// 此函數(shù)可以訪問(wèn)變量 script1,script2 和 script3
one();
two();
three();
});
});
});
這段代碼做了相同的事兒:按順序加載 3 個(gè)腳本。但它是“向右增長(zhǎng)”的。所以會(huì)有和使用回調(diào)函數(shù)一樣的問(wèn)題。
剛開(kāi)始使用 promise 的人可能不知道 promise 鏈,所以他們就這樣寫(xiě)了。通常,鏈?zhǔn)绞鞘走x。
有時(shí)候直接寫(xiě) .then
也是可以的,因?yàn)榍短椎暮瘮?shù)可以訪問(wèn)外部作用域。在上面的例子中,嵌套在最深層的那個(gè)回調(diào)(callback)可以訪問(wèn)所有變量 script1
,script2
和 script3
。但這是一個(gè)例外,而不是一條規(guī)則。
Thenables
確切地說(shuō),處理程序返回的不完全是一個(gè) promise,而是返回的被稱(chēng)為 “thenable” 對(duì)象 —— 一個(gè)具有方法
.then
的任意對(duì)象。它會(huì)被當(dāng)做一個(gè) promise 來(lái)對(duì)待。
這個(gè)想法是,第三方庫(kù)可以實(shí)現(xiàn)自己的“promise 兼容(promise-compatible)”對(duì)象。它們可以具有擴(kuò)展的方法集,但也與原生的 promise 兼容,因?yàn)樗鼈儗?shí)現(xiàn)了
.then
方法。
這是一個(gè) thenable 對(duì)象的示例:
class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // 1 秒后使用 this.num*2 進(jìn)行 resolve setTimeout(() => resolve(this.num * 2), 1000); // (**) } } new Promise(resolve => resolve(1)) .then(result => { return new Thenable(result); // (*) }) .then(alert); // 1000ms 后顯示 2
JavaScript 檢查在
(*)
行中由.then
處理程序返回的對(duì)象:如果它具有名為then
的可調(diào)用方法,那么它將調(diào)用該方法并提供原生的函數(shù)resolve
和reject
作為參數(shù)(類(lèi)似于 executor),并等待直到其中一個(gè)函數(shù)被調(diào)用。在上面的示例中,resolve(2)
在 1 秒后被調(diào)用(**)
。然后,result 會(huì)被進(jìn)一步沿著鏈向下傳遞。
這個(gè)特性允許我們將自定義的對(duì)象與 promise 鏈集成在一起,而不必繼承自
Promise
。
在前端編程中,promise 通常被用于網(wǎng)絡(luò)請(qǐng)求。那么,讓我們一起來(lái)看一個(gè)相關(guān)的擴(kuò)展示例吧。
我們將使用 fetch 方法從遠(yuǎn)程服務(wù)器加載用戶信息。它有很多可選的參數(shù),我們?cè)?nbsp;單獨(dú)的一章 中對(duì)其進(jìn)行了詳細(xì)介紹,但基本語(yǔ)法很簡(jiǎn)單:
let promise = fetch(url);
執(zhí)行這條語(yǔ)句,向 url
發(fā)出網(wǎng)絡(luò)請(qǐng)求并返回一個(gè) promise。當(dāng)遠(yuǎn)程服務(wù)器返回 header(是在 全部響應(yīng)加載完成前)時(shí),該 promise 使用一個(gè) response
對(duì)象來(lái)進(jìn)行 resolve。
為了讀取完整的響應(yīng),我們應(yīng)該調(diào)用 response.text()
方法:當(dāng)全部文字內(nèi)容從遠(yuǎn)程服務(wù)器下載完成后,它會(huì)返回一個(gè) promise,該 promise 以剛剛下載完成的這個(gè)文本作為 result 進(jìn)行 resolve。
下面這段代碼向 user.json
發(fā)送請(qǐng)求,并從服務(wù)器加載該文本:
fetch('/article/promise-chaining/user.json')
// 當(dāng)遠(yuǎn)程服務(wù)器響應(yīng)時(shí),下面的 .then 開(kāi)始執(zhí)行
.then(function(response) {
// 當(dāng) user.json 加載完成時(shí),response.text() 會(huì)返回一個(gè)新的 promise
// 該 promise 以加載的 user.json 為 result 進(jìn)行 resolve
return response.text();
})
.then(function(text) {
// ……這是遠(yuǎn)程文件的內(nèi)容
alert(text); // {"name": "iliakan", "isAdmin": true}
});
從 fetch
返回的 response
對(duì)象還包含 response.json()
方法,該方法可以讀取遠(yuǎn)程數(shù)據(jù)并將其解析為 JSON。在我們的例子中,這更加方便,所以我們用這個(gè)方法吧。
為了簡(jiǎn)潔,我們還將使用箭頭函數(shù):
// 同上,但使用 response.json() 將遠(yuǎn)程內(nèi)容解析為 JSON
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan,獲取到了用戶名
現(xiàn)在,讓我們用加載好的用戶信息搞點(diǎn)事情。
例如,我們可以再向 GitHub 發(fā)送一個(gè)請(qǐng)求,加載用戶個(gè)人資料并顯示頭像:
// 發(fā)送一個(gè)對(duì) user.json 的請(qǐng)求
fetch('/article/promise-chaining/user.json')
// 將其加載為 JSON
.then(response => response.json())
// 發(fā)送一個(gè)到 GitHub 的請(qǐng)求
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// 將響應(yīng)加載為 JSON
.then(response => response.json())
// 顯示頭像圖片(githubUser.avatar_url)3 秒(也可以加上動(dòng)畫(huà)效果)
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
這段代碼可以工作,具體細(xì)節(jié)請(qǐng)看注釋。但是,這有一個(gè)潛在的問(wèn)題,一個(gè)新手使用 promise 時(shí)的典型問(wèn)題。
請(qǐng)看 (*)
行:我們?nèi)绾文茉陬^像顯示結(jié)束并被移除 之后 做點(diǎn)什么?例如,我們想顯示一個(gè)用于編輯該用戶或者其他內(nèi)容的表單。就目前而言,是做不到的。
為了使鏈可擴(kuò)展,我們需要返回一個(gè)在頭像顯示結(jié)束時(shí)進(jìn)行 resolve 的 promise。
就像這樣:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// 3 秒后觸發(fā)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
也就是說(shuō),第 (*)
行的 .then
處理程序現(xiàn)在返回一個(gè) new Promise
,只有在 setTimeout
中的 resolve(githubUser)
(**)
被調(diào)用后才會(huì)變?yōu)?settled。鏈中的下一個(gè) .then
將一直等待這一時(shí)刻的到來(lái)。
作為一個(gè)好的做法,異步行為應(yīng)該始終返回一個(gè) promise。這樣就可以使得之后我們計(jì)劃后續(xù)的行為成為可能。即使我們現(xiàn)在不打算對(duì)鏈進(jìn)行擴(kuò)展,但我們之后可能會(huì)需要。
最后,我們可以將代碼拆分為可重用的函數(shù):
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// 使用它們:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
如果 .then
(或 catch/finally
都可以)處理程序返回一個(gè) promise,那么鏈的其余部分將會(huì)等待,直到它狀態(tài)變?yōu)?settled。當(dāng)它被 settled 后,其 result(或 error)將被進(jìn)一步傳遞下去。
這是一個(gè)完整的流程圖:
這兩個(gè)代碼片段是否相等?換句話說(shuō),對(duì)于任何處理程序(handler),它們?cè)谌魏吻闆r下的行為都相同嗎?
promise.then(f1).catch(f2);
對(duì)比:
promise.then(f1, f2);
簡(jiǎn)要回答就是:不,它們不相等:
不同之處在于,如果 f1
中出現(xiàn) error,那么在這兒它會(huì)被 .catch
處理:
promise
.then(f1)
.catch(f2);
……在這兒則不會(huì):
promise
.then(f1, f2);
這是因?yàn)?error 是沿著鏈傳遞的,而在第二段代碼中,f1
下面沒(méi)有鏈。
換句話說(shuō),.then
將 result/error 傳遞給下一個(gè) .then/.catch
。所以在第一個(gè)例子中,在下面有一個(gè) catch
,而在第二個(gè)例子中并沒(méi)有 catch
,所以 error 未被處理。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: