JavaScript Event Loop (事件循環)
理解 Event Loop 是掌握 JavaScript 異步程式設計的關鍵。Event Loop 是讓 JavaScript 這個單執行緒語言能夠處理異步操作的核心機制。
JavaScript 是單執行緒
JavaScript 是單執行緒 (single-threaded) 的語言,意思是一次只能執行一件事。那為什麼我們可以同時處理多個異步操作(如 AJAX 請求、計時器)呢?答案就是 Event Loop。
執行環境的組成
要理解 Event Loop,需要先了解 JavaScript 執行環境的幾個重要組成:
Call Stack(呼叫堆疊)
Call Stack 是一個資料結構,用來追蹤程式目前執行到哪裡。當你呼叫一個函式,它會被推入 (push) 堆疊;當函式執行完畢,它會被彈出 (pop) 堆疊。
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
var result = square(n);
console.log(result);
}
printSquare(4);
執行過程中 Call Stack 的變化:
1. [printSquare]
2. [printSquare, square]
3. [printSquare, square, multiply]
4. [printSquare, square] // multiply 回傳後彈出
5. [printSquare] // square 回傳後彈出
6. [printSquare, console.log]
7. [printSquare] // console.log 執行完彈出
8. [] // printSquare 執行完彈出
Web APIs
瀏覽器提供的 API,如 setTimeout、fetch、DOM 事件等。這些 API 不是 JavaScript 引擎本身的一部分,而是由瀏覽器(或 Node.js)提供。
Callback Queue(回呼佇列)
也稱為 Task Queue 或 Message Queue。當異步操作完成時,它的回呼函式會被放入這個佇列等待執行。
Event Loop
Event Loop 的工作就是不斷檢查 Call Stack 是否為空,如果是空的,就從 Callback Queue 取出一個任務放入 Call Stack 執行。
Event Loop 運作流程
console.log('1');
setTimeout(function() {
console.log('2');
}, 0);
console.log('3');
執行結果是 1, 3, 2,而不是 1, 2, 3。讓我們一步步看發生了什麼:
console.log('1')進入 Call Stack,執行後輸出1,彈出setTimeout進入 Call Stack,瀏覽器設定計時器,setTimeout彈出console.log('3')進入 Call Stack,執行後輸出3,彈出- 計時器到期(即使是 0ms),callback 被放入 Callback Queue
- Event Loop 檢查 Call Stack 為空,將 callback 移入 Call Stack
- 執行 callback,輸出
2
重點:即使 setTimeout 設定 0 毫秒,callback 也不會立即執行,因為它必須等 Call Stack 清空後,才會被 Event Loop 放入執行。
實際範例
範例一:長時間運算阻塞
console.log('開始');
// 模擬長時間運算
var end = Date.now() + 3000;
while (Date.now() < end) {
// 阻塞 3 秒
}
console.log('結束');
在這 3 秒期間,整個頁面會凍結,無法回應任何使用者互動。這是因為 Call Stack 一直被佔用,Event Loop 無法處理其他任務。
範例二:非阻塞做法
console.log('開始');
setTimeout(function() {
console.log('3 秒後');
}, 3000);
console.log('結束'); // 這行會立即執行
使用 setTimeout,主執行緒不會被阻塞,使用者仍可與頁面互動。
Microtask 與 Macrotask
Callback Queue 其實分為兩種:
- Macrotask Queue(宏任務):setTimeout、setInterval、I/O、UI 渲染
- Microtask Queue(微任務):Promise.then、MutationObserver
執行優先順序:
- 執行 Call Stack 中的程式碼
- Call Stack 清空後,先執行所有 Microtask
- Microtask 清空後,執行一個 Macrotask
- 重複步驟 2-3
console.log('1');
setTimeout(function() {
console.log('2');
}, 0);
Promise.resolve().then(function() {
console.log('3');
});
console.log('4');
// 輸出順序:1, 4, 3, 2
解釋:
console.log('1')同步執行setTimeoutcallback 進入 Macrotask QueuePromise.thencallback 進入 Microtask Queueconsole.log('4')同步執行- Call Stack 清空,執行 Microtask,輸出
3 - 執行 Macrotask,輸出
2
更複雜的範例
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function() {
console.log('promise1');
})
.then(function() {
console.log('promise2');
});
console.log('script end');
// 輸出順序:
// script start
// script end
// promise1
// promise2
// setTimeout
實際應用
避免阻塞 UI
如果需要處理大量資料,可以分批處理:
function processLargeArray(array) {
var index = 0;
var chunkSize = 100;
function processChunk() {
var end = Math.min(index + chunkSize, array.length);
for (var i = index; i < end; i++) {
// 處理 array[i]
}
index = end;
if (index < array.length) {
// 讓出執行權,讓 UI 有機會更新
setTimeout(processChunk, 0);
}
}
processChunk();
}
確保 DOM 更新後執行
element.style.display = 'block';
// 使用 setTimeout 確保在 DOM 更新後執行
setTimeout(function() {
element.classList.add('animate');
}, 0);
總結
- JavaScript 是單執行緒,靠 Event Loop 處理異步操作
- Call Stack 執行同步程式碼
- 異步操作完成後,callback 會進入 Queue 等待
- Event Loop 在 Call Stack 清空時,將 Queue 中的任務放入執行
- Microtask(Promise)優先於 Macrotask(setTimeout)執行
理解 Event Loop 對於寫出高效能、不阻塞的 JavaScript 程式碼非常重要。