我們已經掌握了 Promise 與 Async/Await 的語法。今天我們要探討 JavaScript 底層執行核心——事件循環 (Event Loop)。
了解 Event Loop,能幫住徹底弄懂「程式碼的執行順序」,更能看懂 React 的 Batching(批次更新) 機制是如何與微任務 (Microtask) 巧妙結合的。
JavaScript 是單執行緒(Single-threaded)語言,一次只能做一件事。為了處理非同步操作而不卡死畫面,瀏覽器架構了 Event Loop 機制,分為三個核心區域:
Call Stack(呼叫棧):執行同步程式碼的地方,遵守「後進先出 (LIFO)」。
Task Queue / Macrotask Queue(宏任務佇列):存放 setTimeout、setInterval、DOM 事件、I/O 等 Callback。
Microtask Queue(微任務佇列):存放 Promise.then/catch/finally、queueMicrotask、process.nextTick 等 Callback。
┌────────────────────────────────────────┐
│ Call Stack │
└──────────────────┬─────────────────────┘
│ Stack 清空時
▼
┌────────────────────────────────────────┐
│ Microtask Queue │ ◄── 優先清空!(Promise)
└──────────────────┬─────────────────────┘
│ 全部清空後
▼
┌────────────────────────────────────────┐
│ Render / Painting │ ◄── 畫面更新
└──────────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Macrotask Queue │ ◄── 取出一個執行 (setTimeout)
└────────────────────────────────────────┘
Event Loop 的運作法則:
同步任務 先在 Call Stack 執行完畢。
當 Call Stack 清空時,Event Loop 會優先將 Microtask Queue 裡面的任務全部清空。
Microtask 清空後,瀏覽器進行畫面 Rendering(若需要)。
最後從 Macrotask Queue 取出下一個任務放進 Call Stack 執行。
console.log('1. 同步');
setTimeout(() => {
console.log('2. Macrotask (setTimeout)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Microtask (Promise)');
});
console.log('4. 同步');
輸出順序:1. 同步 => 4. 同步 => 3. Microtask (Promise) => 2. Macrotask (setTimeout)
在 React 中,如果你連續呼叫多次 setState,React 不會每呼叫一次就立刻重新渲染一次畫面,而是會將它們「打包批次處理 (Batching)」,只引發一次 Re-render。
在 React 17 以前,Batching 只在 React 原生事件(如 onClick)中生效。若放在非同步 Callback(如 setTimeout 或 fetch)中,Batching 就會失效:
// React 17:setTimeout 內部呼叫兩次 setState 會觸發兩次 Re-render
setTimeout(() => {
setCount(c => c + 1); // 觸發 Re-render 1
setFlag(f => !f); // 觸發 Re-render 2
}, 1000);
React 18 實現了全自動批次更新,不論 State 更新發生在 setTimeout、Promise 還是原生事件中,都會自動打包成一次更新。
而這背後的排程(Scheduler)機制,正是善用了 Event Loop 的 Microtask 階段:
function handleClick() {
// 1. 同步執行:React 將更新任務收集進內部 Queue,但不立刻渲染
setCount(c => c + 1);
setFlag(f => !f);
// 2. React 透過 queueMicrotask (或 Promise) 註冊一個微任務:
// 「請在當前同步程式碼執行完畢後,一次性比對並更新 DOM!」
}
// 3. Call Stack 清空 => 觸發 Microtask => 一次性完成 Batching 與渲染!