初探 JavaScript 之 Event Loop and Promise
2024-07-08 12:51:01
## 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 時,會將其交由其他程序處理。常見的非同步處理方式有兩種:
1. 當 **Call Stack** 執行到屬於 **Web API** 的函式時,函式執行後產生的 Callback 會傳遞給 **Web API** 處理,API 處理完後會將結果傳遞到 **Macrotask Queue**。
2. **Promise** 物件則是在處理「不確定何時、是否會獲得期待的回應」時的一種方便選擇。而 Promise 物件的 Callback 會在 **Microtask Queue** 中排隊。

*圖片太小可參考 [這裡](https://imgur.com/jZAa9U4)*
**Event loop** 會不斷地在 **Call Stack**、**Microtask Queue** 和 **Macrotask Queue** 之間來回檢查是否有待處理的任務,其運作規則為:
1. 優先處理所有 **Call Stack** 任務(FILO, First In Last Out)。
2. 當 **Call Stack** 任務執行完畢後,Event loop 會優先檢查 **Microtask Queue** 的任務,若有任務,則會將「所有任務」傳遞到 **Call Stack** 中執行(FIFO, First In First Out)。
3. 當前兩者任務都結束時,才輪到檢查 **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:
```javascript
new Promise( /* executor */ function(resolve, reject) { ... } );
```
Promise 會存在三種狀態:
1. 當一個 Promise instance 剛建立時,狀態會是 `pending`
3. 如果 resolve callback 任務執行成功,Promise 狀態會變成 `fulfilled`
4. 若任務失敗, Promise 狀態會變成 `rejected`
而我們就可以利用這樣的機制,去設計「接下來」的任務,透過 `then()` 方法,可以直接設計成功或失敗情境後的對應行為。
`then()` 方法最多接受兩個參數;第一個參數是Promise fulfilled 情況的 callback,第二個參數是 Promise rejected 情況的 callback。

我們直接來看 MDN 給的案例:
```javascript
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 ),當電腦出拳後再去分析誰輸誰贏。
這邊先進行一個錯誤示範:
```javascript
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 ,因此最後在畫面上的輸出結果會類似
```bash
Game Start
You win # or You lose
Nice card... let me think 2 sec
```
如果真的要停頓兩秒後再去 resolve Promise,則要將 `RockPaperSissors` 改成以 setTimeout 將 Promise 的 callback 包住,將其設定為 setTimeout 的 callback。
```javascript
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);
});
}
```
參考資料:
- MDN-Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#description
- MDN-Promise.then(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
點擊複製文章連結