promise chaining 大致可以表達成以下的形式,另外昨天還沒有提到的是,如果所有的任務有同樣的錯誤處理,可以把 catch()
接在鏈接的最後:
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
用箭頭函式表達:
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
發送網路請求是前端常常碰到的非同步情境,例如使用 fetch
方法:
let promise = fetch(url);
這個方法會對傳入的 url 請求資源並回傳一個 promise。伺服器回傳 headers 時,這個 promise 會 resolve 一個 response
物件。要讀到完整的回應,可以呼叫幾種 response
的方法,其中一種是 response.json()
,它也會回傳一個 promise,resolve 出 JSON
格式的結果。
假設有個取得使用者資料的操作,可以寫成以下:
fetch('/article/promise-chaining/user.json')
.then(response => response.json()) // 利用 response.json() 將結果解析為 JSON
.then(user => alert(user.name));
現實中,在取到使用者資料 (user
) 後很可能還有後續的工作,例如利用使用者名稱 (user.name
),再去
githubUser.avatar_url
) 三秒後移除 (第 5 行 - 最後)fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`)) // 3
.then(response => response.json()) // 4
.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 鏈看起來很好...嗎!?以前我大概就是這樣照著工作順序鏈接完就結束,然後也看不太出來潛在問題或能怎麼改。以下是幾個可以思考的方向。
第一個點是最後面的 setTimeout
,如果想在移除頭像之後再做點別的事情,現在的程式碼不好做,因為又回到一開始的問題,不知道非同步完成的確切時間點。但如果利用 promise 在移除後發出通知 (resolve),就可以正確接續下去:
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) { // 這裡改成回傳 promise
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = 'promise-avatar-example';
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // 移除頭像後 resolve
}, 3000);
}))
// 這裡會在三秒後執行
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
這也就延伸到一個重點原則:碰到非同步任務都應該回傳 promise。即便現在還用不到,回傳 promise 讓程式碼更靈活。
所有工作比較確定後,或許可以考慮拆分整理程式碼。這件事一開始做有點卡,但實際工作感受是蠻值得做的,因為不同操作或 api 可能看起來彼此獨立,其實有很多共通邏輯,整理之後會比較輕簡、語意清楚。希望之後的文章有機會寫到工作中的整理實例。
// 抽出請求邏輯
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}`));
// ...