Event loop流程介紹
什麼是 Event Loop?
Event Loop 是 JavaScript 用來處理非同步操作的核心機制。JavaScript 是單執行緒(single-threaded)的語言,意味著它一次只能執行一項任務。Event Loop 的作用是管理程式碼的執行順序,特別是處理非同步的回呼函式(callbacks)、Promise、setTimeout 等,讓它們在適當的時機執行。
簡單來說,Event Loop 就像一個「任務調度員」,負責決定什麼時候執行哪些任務,確保非同步操作不會阻塞主執行緒。
Event Loop 的核心組成
要理解 Event Loop 的流程,我們需要先認識幾個關鍵概念:
-
Call Stack(呼叫堆疊):
-
JavaScript 用來執行程式碼的堆疊,遵循「後進先出」(LIFO)的原則。
-
當一個函式被呼叫時,它會被推入堆疊;執行完成後,會被移除。
-
-
Web APIs:
-
瀏覽器提供的 API(例如 setTimeout、fetch、DOM 事件等)負責處理非同步操作。
-
這些 API 不在 JavaScript 引擎內,而是在瀏覽器環境中執行。
-
-
Task Queue(任務佇列):
-
儲存非同步任務的回呼函式,例如 setTimeout 的回呼或事件監聽器的回呼。
-
任務佇列遵循「先進先出」(FIFO)的原則。
-
-
Microtask Queue(微任務佇列):
-
專門處理優先級較高的微任務,例如 Promise 的 then、catch 或 await。
-
微任務佇列的執行優先級高於任務佇列。
-
-
Event Loop(事件循環):
- 不斷檢查 Call Stack 是否為空,並決定是否將 Task Queue 或 Microtask Queue 中的任務推入 Call Stack 執行。
Event Loop 的運作流程
以下是 Event Loop 的詳細流程,步驟清晰且易於理解:
-
執行同步程式碼:
-
JavaScript 引擎首先執行程式碼中的同步部分,所有同步程式碼會直接進入 Call Stack 並立即執行。
-
例如,console.log 這樣的同步操作會直接推入堆疊,執行後立即移除。
-
-
處理非同步操作:
-
當遇到非同步操作(例如 setTimeout 或 fetch),JavaScript 會將這些操作交給 Web APIs 處理。
-
Web APIs 會在背景執行非同步任務(例如計時器或網路請求),並在完成後將回呼函式推入 Task Queue 或 Microtask Queue。
-
-
檢查 Call Stack 是否為空:
- Event Loop 不斷監控 Call Stack。如果 Call Stack 為空,Event Loop 會檢查是否有任務需要執行。
-
優先執行 Microtask Queue:
- 如果 Microtask Queue 中有任務(例如 Promise 的回呼),Event Loop 會優先將所有微任務依次推入 Call Stack 執行,直到 Microtask Queue 清空。
-
執行 Task Queue:
-
當 Microtask Queue 清空後,Event Loop 才會從 Task Queue 中取出一個任務,推入 Call Stack 執行。
-
每個任務執行完成後,Event Loop 會再次檢查 Microtask Queue 是否有新任務。
-
-
重複循環:
- 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('結束執行');
執行步驟詳解
-
同步程式碼執行:
-
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,輸出「結束執行」,然後移除。
-
-
Call Stack 清空:
- 此時 Call Stack 為空,Event Loop 開始檢查。
-
執行 Microtask Queue:
- Microtask Queue 中有 Promise 的回呼,Event Loop 將其推入 Call Stack,輸出「Promise 回呼(Microtask Queue)」。
-
執行 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。
常見問題與解答
-
為什麼 setTimeout(..., 0) 不是立即執行?
- 即使延遲設為 0,setTimeout 的回呼仍會進入 Task Queue,必須等待 Call Stack 和 Microtask Queue 清空後才執行。
-
Microtask 和 Task 的優先級差異?
- Microtask(例如 Promise)優先級高於 Task(例如 setTimeout)。Event Loop 會先清空 Microtask Queue,再處理 Task Queue。
-
如何避免 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 和 同步 2 是同步程式碼,立即執行並輸出。
-
Microtask Queue 優先於 Task Queue:
- 在同步程式碼執行完後,Event Loop 優先處理 Microtask Queue 中的 Promise 1 和 Promise 2,因此它們在 setTimeout 之前輸出。
-
巢狀操作的影響:
-
Promise 1 的回呼中有一個 setTimeout,它的回呼被推入 Task Queue,必須等待 Microtask Queue 清空。
-
setTimeout 1 的回呼中有一個 Promise,它的回呼被推入 Microtask Queue,並在 setTimeout 1 執行後立即處理(因為 Microtask Queue 優先)。
-
-
Task Queue 按順序執行:
- Task Queue 中的任務(setTimeout 1、setTimeout 2、Promise 1 內的 setTimeout)按先進先出的順序執行。
總結
Event Loop 是 JavaScript 非同步的核心,負責協調同步程式碼、非同步任務(Task Queue)和微任務(Microtask Queue)的執行順序。它的運作流程簡單來說是:
-
執行同步程式碼。
-
將非同步任務交給 Web APIs,完成後進入 Task Queue 或 Microtask Queue。
-
Event Loop 檢查 Call Stack,優先執行 Microtask Queue,再執行 Task Queue。