在了解 Promise 前首先來了解什麼是同步與非同步。
同步(Synchronous)、非同步(Asynchronous)
假設今天回到家會做開門、脫鞋子、拿起鞋子、進門、關門,以同步來講就是:
開門 → 脫鞋子 → 拿起鞋子 → 進門 → 關門
簡單來說就是前一個動作執行完才能執行下一個動作。那這時有人會說,可是今天我想先脫鞋拿起鞋子後再進門關門不行嗎?
當然可以,這種先做其他可以做的事,並且不會影響其他事情的就叫做非同步。
在 JavaScript 裡當遇到非同步事件時,會把它放到一個叫做事件佇列(Event Queue)的地方,等待同步事件處理完之後才會去處理非同步事件。
callback hell
在 Promise 還沒出現之前,當我們希望許多非同步的 function
能依照想要的順序執行時,就會在非同步 function
裡呼叫另一個非同步 function
,當數量一多的時候,就會變成過度巢狀並且難以閱讀,也就是俗稱的 callback hell。
用 Google 搜尋 callback hell 常會出現的梗圖。
圖片來源
接下來就來了解 Promise 物件是怎麼改善 callback hell 的問題。
狀態和流程
在進入 Promise 同時就會進入 pending
等待事件完成,當事件完成後會依據結果回傳成功回應或是拒絕理由:
- 當結果為成功時(fulfilled)會用
resolve
變數來傳送訊息。 - 當結果為拒絕,操作失敗時(rejected)會用
reject
變數來傳送訊息。
而接收訊息的地方在 Promise 是用 then()
和 catch()
來分別接收 resolve
、reject
訊息。
const toEatDinner = () => {
const randomNum = Math.floor(Math.random() * 2);
return new Promise((resolve, reject) => {
if(randomNum){
setTimeout(() => resolve('成功吃到晚餐(fulfilled)'), 5000);
}else{
reject('沒帶錢包,沒辦法吃晚餐(rejected)');
};
});
};
toEatDinner()
.then(res => console.log(res)) // 接收 resolve 的訊息
.catch(err => console.log(err)); // 接收 reject 的訊息
Promise.all、Promise.race
Promise.race
使用 Promise.race
不管事件成功與否都只會回傳第一個完成的事件的結果。
const toEatDinner = (status, food, time) => {
return new Promise((resolve, reject) => {
setTimeout(() =>{
if(status){
resolve(`成功吃到 ${food}`);
}else{
reject(`沒錢了,沒辦法吃 ${food}`);
};
}, time);
});
};
Promise.race([
toEatDinner(1, 'chicken', 2000),
toEatDinner(0, 'pizza', 1000),
toEatDinner(1, 'rice', 3000)
])
.then(res => console.log(res))
.catch(err => console.log(err)); // 沒錢了,沒辦法吃 pizza
Promise.all
如果想要同時執行多個 Promise,可以使用 Promise.all()
,但要注意的是當其中一個 Promise 為 reject
時,那麼全部的 Promise 都會視為失敗。
const toEatDinner = (food, time) => {
const randomNum = Math.floor(Math.random() * 2);
return new Promise((resolve, reject) => {
if(randomNum){
setTimeout(() => resolve(`成功吃到 ${food}`), time);
}else{
reject(`沒錢了,沒辦法吃 ${food}`);
};
});
};
Promise.all([
toEatDinner('chicken', 3000),
toEatDinner('pizza', 1000),
toEatDinner('rice', 2000),
])
.then(res => console.log(res))
.catch(err => console.log(err));
Chain 鏈接方法
如果想要依序執行多個 Promise,可以善用 then()
。
const toEatDinner = (food, time) => {
return new Promise(resolve => {
if(true){
setTimeout(() => resolve(`成功吃到 ${food}`), time);
};
});
};
toEatDinner('chicken', 3000)
.then(res => {
console.log(res);
return toEatDinner('pizza', 1000)
}).then(res => {
console.log(res);
return toEatDinner('rice', 2000)
}).then(res => {
console.log(res);
});
但需要注意的是,當其中有一個結果為失敗,Promise 會跳過中間的 then
找最近的 catch
來接收訊息。
以下面為例,當編號 1 的 Promise 執行完回傳成功訊息並執行編號 2 的 Promise,但當編號 2 的 Promise 執行結果失敗,就會繞過編號 4 的 Promise 跑到下面的 catch
接收錯誤訊息,並且直接執行編號 5 的 Promise。
當編號 2 執行結果失敗時的執行順序:1 → 2 → catch → 5
const toEatDinner = (status, food, time) => {
return new Promise((resolve, reject) => {
if(status){
setTimeout(() => resolve(`成功吃到 ${food}`), time);
}else{
reject(`沒錢了,沒辦法吃 ${food}`);
};
});
};
toEatDinner(1, 'chicken', 3000) // 1
.then(res => {
console.log(res);
return toEatDinner(0, 'pizza', 1000) // 2
}).then(res => {
console.log(res);
return toEatDinner(1, 'rice', 2000) // 3
}).then(res => {
console.log(res);
return toEatDinner(1, 'sushi', 2000) // 4
}).catch(err => {
console.log(err);
return toEatDinner(1, 'chips', 1000) // 5
}).then(res => {
console.log(res);
});