為什麼要Promise

JS是單執行緒的程式語言,透過callback的方式實現出非同步的操作。
日常生活中,我們時常都在Promise,我Promise你這件事會做到,完成或失敗就執行指定的任務。

Promise基本用法


假設有一個實際情境,做完任務再拿獎金

1
2
doTask(); // type: Normal Function
takeMyMoney(); // type: Normal Function

但是doTask需要時間呀,獎金也是要任務完成才能拿,如果接任務和拿獎金一起做,似乎不太好。
因此doTask不應該只是一個Function,它應該要是一個Promise。
而且做完任務才能執行以下的任務。

1
2
3
4
doTask() // type: Promise
.then(() => {
takeMyMoney(); // type: Normal Function
})

一定要確認doTask回傳為Promise型態才可以使用非同步,否則會噴錯
錯誤的做法:

1
2
3
4
doTask() // type: Normal Function
.then(() => { // error
takeMyMoney(); // type: Normal Function
})

也可以使用ES6+的方法使用async await改變開發的閱讀體驗:

1
2
3
4
async function() { // make sure this is async function
await doTask(); // type: Normal Function
takeMyMoney(); // type: Normal Function
};

Promise的error handling

可是doTask有成功也有失敗呀?總不能失敗了也要拿錢給他吧?

callback寫法

可以調整一下寫法:

1
2
3
4
5
6
7
8
9
10
11
12
doTask()
.then((response) => {
if(response.result === 'success') {
takeMyMoney();
} else if(response.result === 'failure') {
throw 'too hard';
}
})
.catch((error) => {
// ...
console.log(error);
});

doTask會回傳結果為response,then將收到的結果判斷成功還是失敗
成功才能拿獎金,失敗則回傳失敗的原因做其他例外處理

ES6+寫法

也可以搭配使用async await的寫法,避免後續維護寫出callback hell的程式

1
2
3
4
5
6
7
8
9
10
11
try {
const response = await doTask();
if(response.result === 'success') {
takeMyMoney();
} else if(response.result === 'failure') {
throw 'too hard';
}
} catch(error) {
// ...
console.log(error);
}

Promise進階用法

前面介紹Promise的基本觀念之後,再來就可以做更進一步的操作了
不過有些Promise事件並不需要過度去依賴非同步來達成結果
今天如果一個頁面需要進行多個動作時,你會怎麼做呢?
或是換句話說,今天你去外面點餐,你會怎麼點?

狀況題

你的任務如下:

1
2
3
4
5
goOutside(); // Normal Function
orderHamburger(); // Promise
orderSubway(); // Promise
orderDrink(); // Promise
goHome(); // Normal Function

今天中午肚子餓,走到餐廳幫家裡的人買東西,你要吃漢堡,你哥要吃潛艇堡,你姐要吃喝飲料,這時候你點餐的時候你會怎麼點?
(假設店員數量充足不忙碌的情況下)

提示:直接跑是不可行的,因為點餐都是非同步,所以直接執行就會goOutside再goHome,不符合預期情境。

迷:我知道,都加上await對吧?

1
2
3
4
5
goOutside(); // Normal Function
await orderHamburger(); // Promise
await orderSubway(); // Promise
await orderDrink(); // Promise
goHome(); // Normal Function

看起來似乎可行,因為點完餐再回家,餐點都帶回家了,合理合理。
可是這個執行模式似乎有一點瑕疵,你看出來了嗎?
如果再翻譯成callback的寫法,你大概就比較看得出來了:

1
2
3
4
5
6
7
8
9
goOutside(); // Function
orderHamburger()
.then(() => {
orderSubway()
.then(() => {
orderDrink();
})
})
goHome(); // Function

翻譯故事

1
2
3
4
5
6
7
8
9
10
11
12
你:我要一份漢堡
店員:一共是XX元謝謝
(等待)
店員:這是你的漢堡,祝你用餐愉快
你:我要一份潛艇堡
店員:一共是XX元謝謝
(等待)
店員:這是你的潛艇堡,祝你用餐愉快
你:我要一份飲料
店員:一共是XX元謝謝
(等待)
店員:這是你的飲料,祝你用餐愉快

你聽完之後有什麼感想?為什麼不一次點完餐比較快?難道是要賺三張抽獎券嗎(X
所以應該變成這樣才對:

1
2
3
4
你:我要一份漢堡,一份潛艇堡,一份飲料
店員:一共是XX元謝謝
(等待)
店員:這是你的餐點,祝你用餐愉快

簡潔多了~那麼我們再翻譯回程式吧,可以怎麼做呢?

Promise.all


Promise.all也會回傳一個Promise,裡面所有的Promise完成成功之後就會回傳被resolve的Promise。
因此我們可以把剛剛要一起執行的任務寫在一起。

1
2
3
goOutside(); // Normal Function
await Promise.all([orderHamburger(), orderSubway(), orderDrink()]);
goHome(); // Normal Function

這樣就不用浪費過多的時間了,讓我們再來修飾一下:
回傳的結果也會是一個array,包含所有Promise的結果

1
2
3
4
5
goOutside(); // Normal Function
const responses = await Promise.all([orderHamburger(), orderSubway(), orderDrink()]);
if(responses.every(response => response.result === 'success')) {
goHome(); // Normal Function
}

不過Promise.all有一個小問題需要注意
請看mozilla文件說明:
當任一個陣列成員被拒絕則 Promise.all 被拒絕。例如,若傳入四個將在一段時間後被解決的 promises,而其中一個立刻被拒絕,則 Promise.all 將立刻被拒絕。

重點:Promise.all具有快速回傳失敗的特性,只要發生問題就會中斷

因此只要有一個商品缺貨不能做,那我就無法把所有商品帶回家了
另外在error handling也不太好追蹤,假設有多個Promise可能有問題,他只會回傳一個:

1
2
3
4
5
6
7
goOutside(); // Normal Function
const responses = await Promise.all([orderHamburger(), orderSubway(), orderDrink()]);
if(responses.every(response => response.result === 'success')) {
goHome(); // Normal Function
} else {
console.log('error: ', responses);
}

可是我就算缺貨也要帶回家給想吃的人呀?那我們可以怎麼做呢

Promise.allSettled


ES2020之後,你可以使用Promise.allSettled加強你的error handling體驗
總是會回傳所有Promise成功與失敗的結果,讓你更好撰寫邏輯

1
2
3
4
5
6
7
8
9
goOutside(); // Normal Function
const responses = await Promise.allSettled([orderHamburger(), orderSubway(), orderDrink()]);
if(responses.every(response => response.result === 'success')) {
goHome(); // Normal Function
} else if(responses.some(response => response.result === 'failure')) {
responses.forEach(error => {
console.log('error: ', error);
});
}

該用all還是allSettled

你需要考慮的部分大概有:

  1. 瀏覽器相容性
    • 目前大概舊版的Edge會比較有問題之外,大部分是可以支援的,可以參考看看caniuse
  2. 比較需求
    • all:當其中一個Promise rejected馬上結束
    • allSettled:絕對不會被rejected,因此邏輯可以直接寫在then或是不用寫try catch,需另外解析array內的resolved與rejected。

進階補充

以下是補充額外的資訊,可以斟酌看一下

進一步認識Promise

上面為了簡單解釋,因此審略了一部份東西補充到這裡
Promise本身其實是有狀態的:

  1. pending:初始狀態
    • 尚未被fulfilled或rejected
  2. fulfilled:成功完成
    • onFulfilled/resolve
  3. rejected:操作失敗
    • onRejected/reject

如何自己實作Promise all

Promise all自己本身會回傳Promise,所以在裡面可以直接寫return Promise
再分別解決每一個Promise,直到所有的Promise resolved

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Promise.all
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let solvedCount = 0;

promises.forEach((promise, index) => {
// Promise.resolve(value) for test
promise
.then(result => {
results[index] = result;
solvedCount += 1;

// 全部完成則resolve
if (solvedCount === promises.length) {
resolve(results);
}
}).catch(err => reject(err));
});
});
}

參考資料

Promise.allSettled() VS Promise.all() in JavaScript
What is a Promise