我們回顧壹下 簡介:回調 壹章中提到的問題:我們有壹系列的異步任務要壹個接壹個地執行 —— 例如,加載腳本。我們如何寫出更好的代碼呢?
Promise 提供了壹些方案來做到這壹點。
在本章中,我們將壹起學習 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)鏈進行傳遞 result。
運行流程如下:
初始 promise 在 1 秒後 resolve (*)
,
然後 .then
處理程序被調用 (**)
,它又創建了壹個新的 promise(以 2
作爲值 resolve)。
下壹個 then
(***)
得到了前壹個 then
的值,對該值進行處理(*2)並將其傳遞給下壹個處理程序。
……依此類推。
隨著 result 在處理程序鏈中傳遞,我們可以看到壹系列的 alert
調用:1
→ 2
→ 4
。
這樣之所以是可行的,是因爲每個對 .then
的調用都會返回了壹個新的 promise,因此我們可以在其之上調用下壹個 .then
。
當處理程序返回壹個值時,它將成爲該 promise 的 result,所以將使用它調用下壹個 .then
。
新手常犯的壹個經典錯誤:從技術上講,我們也可以將多個 .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;相反,它們之間彼此獨立運行處理任務。
這是它的壹張示意圖(妳可以將其與上面的鏈式調用做壹下比較):
在同壹個 promise 上的所有 .then
獲得的結果都相同 —— 該 promise 的結果。所以,在上面的代碼中,所有 alert
都顯示相同的內容:1
。
實際上我們極少遇到壹個 promise 需要多個處理程序的情況。使用鏈式調用的頻率更高。
.then(handler)
中所使用的處理程序(handler)可以創建並返回壹個 promise。
在這種情況下,其他的處理程序將等待它 settled 後再獲得其結果。
例如:
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 秒後它會進行 resolve,然後 result(resolve
的參數,在這裏它是 result*2
)被傳遞給第二個 .then
的處理程序。這個處理程序位于 (**)
行,它顯示 2
,並執行相同的行爲。
所以輸出與前面的示例相同:1 → 2 → 4,但是現在在每次 alert
調用之間會有 1 秒鍾的延遲。
返回 promise 使我們能夠構建異步行爲鏈。
讓我們將本章所講的這個特性與在 上壹章 中定義的 promise 化的 loadScript
結合使用,按順序依次加載腳本:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(function(script) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .then(function(script) { return loadScript("https://javascript.info/article/promise-chaining/three.js"); }) .then(function(script) { // 使用在腳本中聲明的函數 // 以證明腳本確實被加載完成了 one(); two(); three(); });
我們可以用箭頭函數來重寫代碼,讓其變得簡短壹些:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/three.js")) .then(script => { // 腳本加載完成,我們可以在這兒使用腳本中聲明的函數 one(); two(); three(); });
在這兒,每個 loadScript
調用都返回壹個 promise,並且在它 resolve 時下壹個 .then
開始運行。然後,它啓動下壹個腳本的加載。所以,腳本是壹個接壹個地加載的。
我們可以向鏈中添加更多的異步行爲。請注意,代碼仍然是“扁平”的 —— 它向下增長,而不是向右。這裏沒有“厄運金字塔”的迹象。
從技術上講,我們可以向每個 loadScript
直接添加 .then
,就像這樣:
loadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/three.js").then(script3 => { // 此函數可以訪問變量 script1,script2 和 script3 one(); two(); three(); }); }); });
這段代碼做了相同的事兒:按順序加載 3 個腳本。但它是“向右增長”的。所以會有和使用回調函數壹樣的問題。
剛開始使用 promise 的人可能不知道 promise 鏈,所以他們就這樣寫了。通常,鏈式是首選。
有時候直接寫 .then
也是可以的,因爲嵌套的函數可以訪問外部作用域。在上面的例子中,嵌套在最深層的那個回調(callback)可以訪問所有變量 script1
,script2
和 script3
。但這是壹個例外,而不是壹條規則。
Thenables
確切地說,處理程序返回的不完全是壹個 promise,而是返回的被稱爲 “thenable” 對象 —— 壹個具有方法 .then
的任意對象。它會被當做壹個 promise 來對待。
這個想法是,第三方庫可以實現自己的“promise 兼容(promise-compatible)”對象。它們可以具有擴展的方法集,但也與原生的 promise 兼容,因爲它們實現了 .then
方法。
這是壹個 thenable 對象的示例:
class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // 1 秒後使用 this.num*2 進行 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
的可調用方法,那麽它將調用該方法並提供原生的函數 resolve
和 reject
作爲參數(類似于 executor),並等待直到其中壹個函數被調用。在上面的示例中,resolve(2)
在 1 秒後被調用 (**)
。然後,result 會被進壹步沿著鏈向下傳遞。
這個特性允許我們將自定義的對象與 promise 鏈集成在壹起,而不必繼承自 Promise
。
在前端編程中,promise 通常被用于網絡請求。那麽,讓我們壹起來看壹個相關的擴展示例吧。
我們將使用 fetch 方法從遠程服務器加載用戶信息。它有很多可選的參數,我們在 單獨的壹章 中對其進行了詳細介紹,但基本語法很簡單:
let promise = fetch(url);
執行這條語句,向 url
發出網絡請求並返回壹個 promise。當遠程服務器返回 header(是在 全部響應加載完成前)時,該 promise 使用壹個 response
對象來進行 resolve。
爲了讀取完整的響應,我們應該調用 response.text()
方法:當全部文字內容從遠程服務器下載完成後,它會返回壹個 promise,該 promise 以剛剛下載完成的這個文本作爲 result 進行 resolve。
下面這段代碼向 user.json
發送請求,並從服務器加載該文本:
fetch('https://javascript.info/article/promise-chaining/user.json') // 當遠程服務器響應時,下面的 .then 開始執行 .then(function(response) { // 當 user.json 加載完成時,response.text() 會返回壹個新的 promise // 該 promise 以加載的 user.json 爲 result 進行 resolve return response.text(); }) .then(function(text) { // ……這是遠程文件的內容 alert(text); // {"name": "iliakan", "isAdmin": true} });
從 fetch
返回的 response
對象還包含 response.json()
方法,該方法可以讀取遠程數據並將其解析爲 JSON。在我們的例子中,這更加方便,所以我們用這個方法吧。
爲了簡潔,我們還將使用箭頭函數:
// 同上,但使用 response.json() 將遠程內容解析爲 JSON fetch('https://javascript.info/article/promise-chaining/user.json') .then(response => response.json()) .then(user => alert(user.name)); // iliakan,獲取到了用戶名
現在,讓我們用加載好的用戶信息搞點事情。
例如,我們可以再向 GitHub 發送壹個請求,加載用戶個人資料並顯示頭像:
// 發送壹個對 user.json 的請求 fetch('https://javascript.info/article/promise-chaining/user.json') // 將其加載爲 JSON .then(response => response.json()) // 發送壹個到 GitHub 的請求 .then(user => fetch(`https://api.github.com/users/${user.name}`)) // 將響應加載爲 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); // (*) });
這段代碼可以工作,具體細節請看注釋。但是,這有壹個潛在的問題,壹個新手使用 promise 時的典型問題。
請看 (*)
行:我們如何能在頭像顯示結束並被移除 之後 做點什麽?例如,我們想顯示壹個用于編輯該用戶或者其他內容的表單。就目前而言,是做不到的。
爲了使鏈可擴展,我們需要返回壹個在頭像顯示結束時進行 resolve 的 promise。
就像這樣:
fetch('https://javascript.info/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 秒後觸發 .then(githubUser => alert(`Finished showing ${githubUser.name}`));
也就是說,第 (*)
行的 .then
處理程序現在返回壹個 new Promise
,只有在 setTimeout
中的 resolve(githubUser)
(**)
被調用後才會變爲 settled。鏈中的下壹個 .then
將壹直等待這壹時刻的到來。
作爲壹個好的做法,異步行爲應該始終返回壹個 promise。這樣就可以使得之後我們計劃後續的行爲成爲可能。即使我們現在不打算對鏈進行擴展,但我們之後可能會需要。
最後,我們可以將代碼拆分爲可重用的函數:
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('https://javascript.info/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => alert(`Finished showing ${githubUser.name}`)); // ...
如果 .then
(或 catch/finally
都可以)處理程序返回壹個 promise,那麽鏈的其余部分將會等待,直到它狀態變爲 settled。當它被 settled 後,其 result(或 error)將被進壹步傳遞下去。
這是壹個完整的流程圖:
這兩個代碼片段是否相等?換句話說,對于任何處理程序(handler),它們在任何情況下的行爲都相同嗎?
promise.then(f1).catch(f2);
對比:
promise.then(f1, f2);
簡要回答就是:不,它們不相等:
不同之處在于,如果 f1
中出現 error,那麽在這兒它會被 .catch
處理:
promise .then(f1) .catch(f2);
……在這兒則不會:
promise .then(f1, f2);
這是因爲 error 是沿著鏈傳遞的,而在第二段代碼中,f1
下面沒有鏈。
換句話說,.then
將 result/error 傳遞給下壹個 .then/.catch
。所以在第壹個例子中,在下面有壹個 catch
,而在第二個例子中並沒有 catch
,所以 error 未被處理。