iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0
自我挑戰組

JS 加強筆記系列 第 8

Day 08: promise chaining

  • 分享至 

  • xImage
  •  

有時候會需要依序執行多個非同步任務,像是先前使用 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 handler 回傳 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 中間會有一秒間隔。

再看 loadScript

先前 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。
  • 不回傳任何東西:pundefined fulfilled。
  • 拋出錯誤:p 以該錯誤為值 rejected。
  • 回傳一個 fulfilled promise:p 以該 promise 的值 fulfilled。
  • 回傳一個 rejected promise:p 以該 promise 的值 rejected。
  • 回傳一個 pending promise:p 維持 pending,等 promise 有結果後立刻轉為 fulfilled/ rejected 。

上一篇
Day 07:小練習
下一篇
Day 09:promise chaining 之比較寫實的例子
系列文
JS 加強筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言