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