WeHelp
初探 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** 中排隊。 ![image](wehelp-storage://d8bdc17b9bafb2c66d048d6a79aacca9) *圖片太小可參考 [這裡](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**。) ![image](wehelp-storage://060821621d657ded65d6002f254a63ec) --- ## 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。 ![Flowchart showing how the Promise state transitions between pending, fulfilled, and rejected via then/catch handlers. A pending promise can become either fulfilled or rejected. If fulfilled, the "on fulfillment" handler, or first parameter of the then() method, is executed and carries out further asynchronous actions. If rejected, the error handler, either passed as the second parameter of the then() method or as the sole parameter of the catch() method, gets executed.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/promises.png) 我們直接來看 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