有時候會需要依序執行多個非同步任務,像是先前使用 loadScript
載入多個檔案。這種時候 promise 的一個處理方式是使用鏈接 (promise chaining)。
先用最簡單的例子看鏈接的結果,執行過程中會依序出現 1, 2, 4 的 alert:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
})
.then(function(result) {
alert(result); // 1, 第一個 promise 的結果
return result * 2;
})
.then(function(result) {
alert(result); // 2, 前面 then handler 回傳的結果
return result * 2;
})
.then(function(result) {
alert(result); // 4, 前面 then handler 回傳的結果
return result * 2;
});
之所以可以這樣串連是因為每一次呼叫 then
都會回傳一個新的 promise,這個新 promise 會以 then
裡面處理函式 (handler) 所回傳的值 resolve,後面的 then
就可以接到。
相對於上面的例子.這樣的情況並非鏈接:
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 分別接了三次,每個處理函式各自獨立,所以會接到同樣的結果。
除了回傳一個值,then
處理函式也可以回傳 promise [註1]。這種時候後面的處理函式就會等待 promise 狀態確定,再得到 promise resolve 的值作為結果。
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 handler 都回傳 promise,promise 都在一秒後傳出結果。所以執行完會跟上面第一個例子結果一樣,只是每個 alert 中間會有一秒間隔。
先前 promise 版的 loadScript
函式:
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);
});
}
如果想要依序載入多個檔案,就可以使用鏈接:
loadScript('one.js')
.then(function(script) {
return loadScript('two.js');
})
.then(function(script) {
return loadScript('three.js');
})
.then(function(script) {
// 檔案都載入後,可以使用裡面的函式
one();
two();
three();
});
不過,這個例子光這樣寫可能沒有表達很明顯鏈接適合的情境:連續用特定順序做非同步任務。也就是每個任務會得到前一個任務的結果,並且在前一個任務結束後執行。(提醒自己未來多思考需求,不要只是為鏈而鏈。)
另外一個問題是,loadScript
本身就會回傳 promise,這樣有一定要用鏈接嗎?如果不使用鏈接 (或是假設 then
並沒有鏈接功能),是也可以把全部的 then
直接接在 loadScript
上,就會變成這個樣子:
loadScript('one.js')
.then(script1 => {
loadScript('two.js')
.then(script2 => {
loadScript('three.js')
.then(script3 => {
// 這層取得到 script1, script2, script3 所有參數
one();
two();
three();
});
});
});
這樣還是會有像 callback 不斷巢狀內縮的問題,也就是原本想利用 promise 避免的問題又出現了。可能有少數例外會這樣做,但大部分時候鏈接是優先選擇。後面會再討論到一點巢狀的例子。
[註1] then handler 可能最常見回傳 promise,但它可以回傳不同值,並且會有不同表現,這裡整理一下。
假設呼叫 then
時回傳的新 promise 為 p
,若 then handler
p
以該值 fulfilled。p
以 undefined
fulfilled。p
以該錯誤為值 rejected。p
以該 promise 的值 fulfilled。p
以該 promise 的值 rejected。p
維持 pending,等 promise 有結果後立刻轉為 fulfilled/ rejected 。