Skip to main content

Event loop流程介紹

什麼是 Event Loop?

Event Loop 是 JavaScript 用來處理非同步操作的核心機制。JavaScript 是單執行緒(single-threaded)的語言,意味著它一次只能執行一項任務。Event Loop 的作用是管理程式碼的執行順序,特別是處理非同步的回呼函式(callbacks)、Promise、setTimeout 等,讓它們在適當的時機執行。

簡單來說,Event Loop 就像一個「任務調度員」,負責決定什麼時候執行哪些任務,確保非同步操作不會阻塞主執行緒。


Event Loop 的核心組成

要理解 Event Loop 的流程,我們需要先認識幾個關鍵概念:

  1. Call Stack(呼叫堆疊)

    • JavaScript 用來執行程式碼的堆疊,遵循「後進先出」(LIFO)的原則。

    • 當一個函式被呼叫時,它會被推入堆疊;執行完成後,會被移除。

  2. Web APIs

    • 瀏覽器提供的 API(例如 setTimeout、fetch、DOM 事件等)負責處理非同步操作。

    • 這些 API 不在 JavaScript 引擎內,而是在瀏覽器環境中執行。

  3. Task Queue(任務佇列)

    • 儲存非同步任務的回呼函式,例如 setTimeout 的回呼或事件監聽器的回呼。

    • 任務佇列遵循「先進先出」(FIFO)的原則。

  4. Microtask Queue(微任務佇列)

    • 專門處理優先級較高的微任務,例如 Promise 的 then、catch 或 await。

    • 微任務佇列的執行優先級高於任務佇列。

  5. Event Loop(事件循環)

    • 不斷檢查 Call Stack 是否為空,並決定是否將 Task Queue 或 Microtask Queue 中的任務推入 Call Stack 執行。

Event Loop 的運作流程

以下是 Event Loop 的詳細流程,步驟清晰且易於理解:

  1. 執行同步程式碼

    • JavaScript 引擎首先執行程式碼中的同步部分,所有同步程式碼會直接進入 Call Stack 並立即執行。

    • 例如,console.log 這樣的同步操作會直接推入堆疊,執行後立即移除。

  2. 處理非同步操作

    • 當遇到非同步操作(例如 setTimeout 或 fetch),JavaScript 會將這些操作交給 Web APIs 處理。

    • Web APIs 會在背景執行非同步任務(例如計時器或網路請求),並在完成後將回呼函式推入 Task Queue 或 Microtask Queue。

  3. 檢查 Call Stack 是否為空

    • Event Loop 不斷監控 Call Stack。如果 Call Stack 為空,Event Loop 會檢查是否有任務需要執行。
  4. 優先執行 Microtask Queue

    • 如果 Microtask Queue 中有任務(例如 Promise 的回呼),Event Loop 會優先將所有微任務依次推入 Call Stack 執行,直到 Microtask Queue 清空。
  5. 執行 Task Queue

    • 當 Microtask Queue 清空後,Event Loop 才會從 Task Queue 中取出一個任務,推入 Call Stack 執行。

    • 每個任務執行完成後,Event Loop 會再次檢查 Microtask Queue 是否有新任務。

  6. 重複循環

    • Event Loop 持續執行上述步驟,確保所有同步和非同步任務按正確順序執行。

圖解 Event Loop 流程

為了讓你更容易理解,我用文字模擬流程:

1. [同步程式碼] -> 進入 Call Stack -> 執行並移除
2. [非同步操作] -> 交給 Web APIs -> 完成後將回呼推入 Task Queue 或 Microtask Queue
3. Event Loop 檢查:
- Call Stack 是否為空?
- 是:檢查 Microtask Queue
- 執行所有微任務直到清空
- 再檢查 Task Queue,執行一個任務
- 否:等待 Call Stack 清空
4. 重複步驟 3

程式碼範例:觀察 Event Loop 運作

以下是一個完整的 JavaScript 程式碼範例,展示同步程式碼、setTimeout(Task Queue)和 Promise(Microtask Queue)的執行順序。我會逐行解釋,幫助你理解 Event Loop 的運作。

console.log('開始執行');

setTimeout(() => {
console.log('setTimeout 回呼(Task Queue)');
}, 0);

Promise.resolve().then(() => {
console.log('Promise 回呼(Microtask Queue)');
});

console.log('結束執行');

執行步驟詳解

  1. 同步程式碼執行

    • console.log('開始執行') 進入 Call Stack,輸出「開始執行」,然後從堆疊移除。

    • setTimeout 進入 Call Stack,但它的回呼函式交給 Web APIs 處理(計時器設為 0 毫秒)。Web APIs 完成後,將回呼推入 Task Queue

    • Promise.resolve().then(...) 進入 Call Stack,Promise 的回呼被推入 Microtask Queue

    • console.log('結束執行') 進入 Call Stack,輸出「結束執行」,然後移除。

  2. Call Stack 清空

    • 此時 Call Stack 為空,Event Loop 開始檢查。
  3. 執行 Microtask Queue

    • Microtask Queue 中有 Promise 的回呼,Event Loop 將其推入 Call Stack,輸出「Promise 回呼(Microtask Queue)」。
  4. 執行 Task Queue

    • Microtask Queue 清空後,Event Loop 檢查 Task Queue,將 setTimeout 的回呼推入 Call Stack,輸出「setTimeout 回呼(Task Queue)」。

輸出結果

開始執行
結束執行
Promise 回呼(Microtask Queue)
setTimeout 回呼(Task Queue)

為什麼是這個順序?

  • 同步程式碼(console.log)優先執行。

  • Microtask Queue(Promise)優先於 Task Queue(setTimeout),所以 Promise 的回呼在 setTimeout 之前執行。

  • 即使 setTimeout 的延遲設為 0 毫秒,它仍然會被放入 Task Queue,等待 Call Stack 和 Microtask Queue 清空後才執行。


實操建議:如何測試 Event Loop

你可以在瀏覽器的開發者工具(F12 -> Console)中運行以下程式碼,觀察輸出順序:

console.log('同步 1');

setTimeout(() => {
console.log('setTimeout 1');
}, 0);

setTimeout(() => {
console.log('setTimeout 2');
}, 0);

Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});

console.log('同步 2');

預期輸出

同步 1
同步 2
Promise 1
Promise 2
setTimeout 1
setTimeout 2

解釋

  • 同步程式碼(同步 1 和 同步 2)先執行。

  • Promise 的兩個 then 回呼進入 Microtask Queue,按順序執行。

  • 最後執行 Task Queue 中的 setTimeout 1 和 setTimeout 2。


常見問題與解答

  1. 為什麼 setTimeout(..., 0) 不是立即執行?

    • 即使延遲設為 0,setTimeout 的回呼仍會進入 Task Queue,必須等待 Call Stack 和 Microtask Queue 清空後才執行。
  2. Microtask 和 Task 的優先級差異?

    • Microtask(例如 Promise)優先級高於 Task(例如 setTimeout)。Event Loop 會先清空 Microtask Queue,再處理 Task Queue。
  3. 如何避免 Event Loop 阻塞?

    • 避免在同步程式碼中執行耗時操作(例如大迴圈)。

    • 使用非同步 API(如 setTimeout、Promise、async/await)將耗時任務交給 Web APIs 處理。

複雜範例

console.log('同步 1:開始執行');

setTimeout(() => {
console.log('setTimeout 1:進入 Task Queue');
Promise.resolve().then(() => {
console.log('setTimeout 1 內的 Promise:進入 Microtask Queue');
});
}, 0);

Promise.resolve().then(() => {
console.log('Promise 1:進入 Microtask Queue');
setTimeout(() => {
console.log('Promise 1 內的 setTimeout:進入 Task Queue');
}, 0);
});

console.log('同步 2:結束同步程式碼');

setTimeout(() => {
console.log('setTimeout 2:進入 Task Queue');
}, 0);

Promise.resolve().then(() => {
console.log('Promise 2:進入 Microtask Queue');
});

程式碼執行步驟詳解

初始狀態

  • Call Stack:空

  • Microtask Queue:空

  • Task Queue:空

  • Web APIs:空

步驟 1:執行同步程式碼

  • 程式碼:console.log('同步 1:開始執行')

    • 進入 Call Stack,輸出「同步 1:開始執行」,然後從 Call Stack 移除。
  • 狀態

    • Call Stack:空

    • Microtask Queue:空

    • Task Queue:空

    • Web APIs:空

步驟 2:處理第一個 setTimeout

  • 程式碼setTimeout(() => { ... }, 0)

    • setTimeout 進入 Call Stack,立即將回呼函式交給 Web APIs 處理(計時器設為 0 毫秒)。

    • Web APIs 完成計時後,將回呼函式推入 Task Queue

    • Call Stack 中的 setTimeout 移除。

  • 狀態

    • Call Stack:空

    • Microtask Queue:空

    • Task Queue:[setTimeout 1 回呼]

    • Web APIs:空

步驟 3:處理第一個 Promise

  • 程式碼Promise.resolve().then(() => { ... })

    • Promise.resolve() 進入 Call Stack,立即解析並將 then 的回呼推入 Microtask Queue

    • Call Stack 中的 Promise.resolve 移除。

  • 狀態

    • Call Stack:空

    • Microtask Queue:[Promise 1 回呼]

    • Task Queue:[setTimeout 1 回呼]

    • Web APIs:空

步驟 4:執行第二個同步程式碼

  • 程式碼:console.log('同步 2:結束同步程式碼')

    • 進入 Call Stack,輸出「同步 2:結束同步程式碼」,然後移除。
  • 狀態

    • Call Stack:空

    • Microtask Queue:[Promise 1 回呼]

    • Task Queue:[setTimeout 1 回呼]

    • Web APIs:空

步驟 5:處理第二個 setTimeout

  • 程式碼setTimeout(() => { ... }, 0)

    • setTimeout 進入 Call Stack,將回呼函式交給 Web APIs,完成後推入 Task Queue

    • Call Stack 中的 setTimeout 移除。

  • 狀態

    • Call Stack:空

    • Microtask Queue:[Promise 1 回呼]

    • Task Queue:[setTimeout 1 回呼, setTimeout 2 回呼]

    • Web APIs:空

步驟 6:處理第二個 Promise

  • 程式碼Promise.resolve().then(() => { ... })

    • Promise.resolve() 進入 Call Stack,立即解析並將 then 的回呼推入 Microtask Queue

    • Call Stack 中的 Promise.resolve 移除。

  • 狀態

    • Call Stack:空

    • Microtask Queue:[Promise 1 回呼, Promise 2 回呼]

    • Task Queue:[setTimeout 1 回呼, setTimeout 2 回呼]

    • Web APIs:空

步驟 7:Event Loop 檢查 Microtask Queue

  • Call Stack 為空,Event Loop 檢查 Microtask Queue,發現有兩個微任務。

  • 第一個微任務:Promise 1 回呼

    • 推入 Call Stack,輸出「Promise 1:進入 Microtask Queue」。

    • 回呼內的 setTimeout 進入 Call Stack,將其回呼函式交給 Web APIs,完成後推入 Task Queue

    • Call Stack 清空。

  • 狀態

    • Call Stack:空

    • Microtask Queue:[Promise 2 回呼]

    • Task Queue:[setTimeout 1 回呼, setTimeout 2 回呼, Promise 1 內的 setTimeout 回呼]

    • Web APIs:空

  • 第二個微任務:Promise 2 回呼

    • 推入 Call Stack,輸出「Promise 2:進入 Microtask Queue」。

    • Call Stack 清空。

  • 狀態

    • Call Stack:空

    • Microtask Queue:空

    • Task Queue:[setTimeout 1 回呼, setTimeout 2 回呼, Promise 1 內的 setTimeout 回呼]

    • Web APIs:空

步驟 8:Event Loop 檢查 Task Queue

  • Microtask Queue 已清空,Event Loop 檢查 Task Queue,依次執行任務。

  • 第一個任務:setTimeout 1 回呼

    • 推入 Call Stack,輸出「setTimeout 1:進入 Task Queue」。

    • 回呼內的 Promise.resolve().then(...) 進入 Call Stack,立即解析並將 then 的回呼推入 Microtask Queue

    • Call Stack 清空。

  • 狀態

    • Call Stack:空

    • Microtask Queue:[setTimeout 1 內的 Promise 回呼]

    • Task Queue:[setTimeout 2 回呼, Promise 1 內的 setTimeout 回呼]

    • Web APIs:空

步驟 9:再次檢查 Microtask Queue

  • Event Loop 發現 Microtask Queue 不為空,執行微任務。

  • 微任務:setTimeout 1 內的 Promise 回呼

    • 推入 Call Stack,輸出「setTimeout 1 內的 Promise:進入 Microtask Queue」。

    • Call Stack 清空。

  • 狀態

    • Call Stack:空

    • Microtask Queue:空

    • Task Queue:[setTimeout 2 回呼, Promise 1 內的 setTimeout 回呼]

    • Web APIs:空

步驟 10:繼續執行 Task Queue

  • Microtask Queue 已清空,Event Loop 繼續執行 Task Queue。

  • 第二個任務:setTimeout 2 回呼

    • 推入 Call Stack,輸出「setTimeout 2:進入 Task Queue」。

    • Call Stack 清空。

  • 第三個任務:Promise 1 內的 setTimeout 回呼

    • 推入 Call Stack,輸出「Promise 1 內的 setTimeout:進入 Task Queue」。

    • Call Stack 清空。

  • 最終狀態

    • Call Stack:空

    • Microtask Queue:空

    • Task Queue:空

    • Web APIs:空

預期輸出

根據上述步驟,程式碼的輸出順序如下:

同步 1:開始執行
同步 2:結束同步程式碼
Promise 1:進入 Microtask Queue
Promise 2:進入 Microtask Queue
setTimeout 1:進入 Task Queue
setTimeout 1 內的 Promise:進入 Microtask Queue
setTimeout 2:進入 Task Queue
Promise 1 內的 setTimeout:進入 Task Queue

為什麼是這個順序?

  1. 同步程式碼優先

    • 同步 1 和 同步 2 是同步程式碼,立即執行並輸出。
  2. Microtask Queue 優先於 Task Queue

    • 在同步程式碼執行完後,Event Loop 優先處理 Microtask Queue 中的 Promise 1 和 Promise 2,因此它們在 setTimeout 之前輸出。
  3. 巢狀操作的影響

    • Promise 1 的回呼中有一個 setTimeout,它的回呼被推入 Task Queue,必須等待 Microtask Queue 清空。

    • setTimeout 1 的回呼中有一個 Promise,它的回呼被推入 Microtask Queue,並在 setTimeout 1 執行後立即處理(因為 Microtask Queue 優先)。

  4. Task Queue 按順序執行

    • Task Queue 中的任務(setTimeout 1、setTimeout 2、Promise 1 內的 setTimeout)按先進先出的順序執行。

總結

Event Loop 是 JavaScript 非同步的核心,負責協調同步程式碼、非同步任務(Task Queue)和微任務(Microtask Queue)的執行順序。它的運作流程簡單來說是:

  1. 執行同步程式碼。

  2. 將非同步任務交給 Web APIs,完成後進入 Task Queue 或 Microtask Queue。

  3. Event Loop 檢查 Call Stack,優先執行 Microtask Queue,再執行 Task Queue。