Event loop 簡介
在理解 Promise 前,必須一起了解 JS 當中的 Event loop 運作方式。Event loop 是協調 Call Stack、Macrotask Queue 和 Microtask Queue 的關鍵機制。
由於 JS 是單執行緒 (Single Thread) 的程式語言,當存放在 Heap 當中的物件或是函式被調用時,任務會進入 Call Stack 等待執行。一般情況下,JS 只能處理同步任務(按照步驟執行 Call Stack 中的指令),無法同時執行多個任務。
為了實現非同步 (Asynchronous) 的功能,當 Call Stack 執行到非同步的 code 時,會將其交由其他程序處理。常見的非同步處理方式有兩種:
- 當 Call Stack 執行到屬於 Web API 的函式時,函式執行後產生的 Callback 會傳遞給 Web API 處理,API 處理完後會將結果傳遞到 Macrotask Queue。
- Promise 物件則是在處理「不確定何時、是否會獲得期待的回應」時的一種方便選擇。而 Promise 物件的 Callback 會在 Microtask Queue 中排隊。
圖片太小可參考 這裡
Event loop 會不斷地在 Call Stack、Microtask Queue 和 Macrotask Queue 之間來回檢查是否有待處理的任務,其運作規則為:
- 優先處理所有 Call Stack 任務(FILO, First In Last Out)。
- 當 Call Stack 任務執行完畢後,Event loop 會優先檢查 Microtask Queue 的任務,若有任務,則會將「所有任務」傳遞到 Call Stack 中執行(FIFO, First In First Out)。
- 當前兩者任務都結束時,才輪到檢查 Macrotask Queue 的任務,若有任務,一樣將其傳遞至 Call Stack 執行,「一次執行一個 task」。
(補充:除了 Web API,在 Node.js 中的 I/O 操作與其他一些任務也會進入 Macrotask Queue。 MutationObserver 的 Callback 也會進入 Microtask Queue。)
Promise 物件運作方式
根據上文,我們已經知道 Promise 物件會在 Microtack Queue 中以非同步方式實現。現在就一起來看看 Promise 物件的用法吧!
當一個 Promise 被創建時,該 Promise 可以接收兩個函數作為參數,我們稱之為 executor,分別為 resolveFunc
與 rejectFunc
,詳見下方的 Promise 的 constructor:
new Promise( /* executor */ function(resolve, reject) { ... } );
Promise 會存在三種狀態:
- 當一個 Promise instance 剛建立時,狀態會是
pending
- 如果 resolve callback 任務執行成功,Promise 狀態會變成
fulfilled
- 若任務失敗, Promise 狀態會變成
rejected
而我們就可以利用這樣的機制,去設計「接下來」的任務,透過 then()
方法,可以直接設計成功或失敗情境後的對應行為。
then()
方法最多接受兩個參數;第一個參數是Promise fulfilled 情況的 callback,第二個參數是 Promise rejected 情況的 callback。
我們直接來看 MDN 給的案例:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 300);
});
myPromise
.then(handleFulfilledA, handleRejectedA)
.then(handleFulfilledB, handleRejectedB)
.then(handleFulfilledC, handleRejectedC);
// 生生不息...
由於上方案例 myPromise
狀態為 fulfilled
,因此在執行第一個 then()
時,會執行 handleFulfilledA
,也就是 myPromise resolve 的 "foo"
。如果狀態是 rejected
則會執行 handleRejectedA
。
then()
方法可以返回值,如果有的話會被包裝成另一個 Promise,這時依然可以使用另一個 then()
方法來解析該 Promise 物件,生生不息。
案例探討
舉個跟電腦猜拳的例子,玩家猜拳後,如果要確保兩秒後電腦也有出拳( 設定一個 setTimeOut 跟一個 Promise ),當電腦出拳後再去分析誰輸誰贏。
這邊先進行一個錯誤示範:
let playercard = "paper";
let RockPaperScissors = function () {
console.log("Game Start");
setTimeout(() => {
console.log("Nice card... let me think 2 sec");
}, 2000);
return new Promise((resolve) => {
const computercard = Math.random();
if (computercard < 0.34) {
resolve("rock");
} else if (computercard <= 0.67) {
resolve("paper");
} else {
resolve("scissors");
}
});
}
let playGame = function (playercard) {
RockPaperScissors().then((result) => {
if (playercard === result) {
console.log("Tie");
} else if (
(playercard === "rock" && result === "scissors") ||
(playercard === "scissors" && result === "paper") ||
(playercard === "paper" && result === "rock")
) {
console.log("You win");
} else {
console.log("You lose");
}
}
)}
playGame(playercard)
由於 setTimeout() 屬於 Web API 當中的方法,其 callback 的優先順序低於 Promise 物件的 callback ,因此最後在畫面上的輸出結果會類似
Game Start
You win # or You lose
Nice card... let me think 2 sec
如果真的要停頓兩秒後再去 resolve Promise,則要將 RockPaperSissors
改成以 setTimeout 將 Promise 的 callback 包住,將其設定為 setTimeout 的 callback。
let RockPaperScissors = function () {
console.log("Game Start");
return new Promise((resolve) => {
setTimeout(() => {
console.log("Nice card... let me think 2 sec");
const computercard = Math.random();
if (computercard < 0.34) {
resolve("rock");
} else if (computercard <= 0.67) {
resolve("paper");
} else {
resolve("scissors");
}
}, 2000);
});
}
參考資料: