Skip to main content

React 在 JavaScript Event Loop 中的運作機制:事件處理、狀態更新與非同步操作

什麼是 Event Loop?(快速複習)

JavaScript 是單執行緒的語言,一次只能處理一個任務。Event Loop 是瀏覽器(或 Node.js)用來管理非同步操作的核心機制,它協調以下部分:

  1. Call Stack(調用堆疊):存放當前執行的函數。

  2. Task Queue(任務隊列):存放宏任務(Macrotask),如 setTimeout 或 DOM 事件回調。

  3. Microtask Queue(微任務隊列):存放微任務(Microtask),如 Promise.then 或 queueMicrotask,優先級高於宏任務。

  4. Web APIs:瀏覽器提供的非同步 API,如 DOM 事件或 fetch。

Event Loop 的執行流程:

  1. 檢查 Call Stack 是否為空。

  2. 如果為空,優先執行 Microtask Queue 中的所有任務。

  3. 再執行 Task Queue 中的一個宏任務。

  4. 重複此過程。

React 的運作完全建立在這個機制上,確保 UI 更新高效且一致。

React 的事件處理:SyntheticEvent 與事件委派

React 不直接綁定事件到每個 DOM 元素,而是使用 SyntheticEvent(合成事件) 系統,提供跨瀏覽器一致性和性能優化。事件處理依賴 Event Loop 的宏任務階段。

運作流程

  1. 事件委派:React 只在應用程式的根節點(例如 #root)設置事件監聽器,當事件發生時,根據 event.target 分發到對應元件。

  2. 事件觸發:用戶交互(如點擊)觸發 Web API,將回調推入 Task Queue。

  3. Event Loop 執行:Call Stack 清空後,從 Task Queue 取出回調,執行 React 的處理邏輯,呼叫你的 onClick 等函數。

以下是完整範例,展示事件處理的流程:

import React from "react";

function App() {
const handleClick = () => {
console.log("按鈕被點擊!這是合成事件處理。");
};

return (
<div>
<button onClick={handleClick}>點我試試</button>
</div>
);
}

export default App;

操作步驟

  1. 建立 React 專案:npx create-react-app react-event-demo → cd react-event-demo → npm start。

  2. 將程式碼貼到 src/App.js。

  3. 打開瀏覽器( http://localhost:3000 ),按 F12 開啟 Console,點擊按鈕。

  4. 觀察:事件回調在 Task Queue 中執行,同步處理。


React 的狀態更新:批量處理與微任務

當你調用 setState(類元件)或 useState 的 setter 時,React 不會立即更新狀態,而是將更新排入內部隊列,稍後批量處理。這部分與 Event Loop 的 微任務(Microtask) 密切相關。

為什麼是微任務?

  • React 使用微任務來處理狀態更新,因為微任務在 Call Stack 清空後立即執行(優先於宏任務),允許在同一個事件循環週期內完成批量更新(Batching),避免多次渲染。

  • 在 React 18 中,引入自動批量更新,即使在非同步操作(如 setTimeout)中,更新也會排入微任務隊列。

  • 內部機制:React 的調度器(Scheduler)使用 queueMicrotask 安排更新,確保高效性和一致性。

運作流程

  1. 調用 setCount 時,React 記錄更新到內部隊列,但不立即改變狀態。

  2. Call Stack 清空後,React 將批量更新推入 Microtask Queue。

  3. 執行微任務:合併更新、計算虛擬 DOM 差異(diffing)、實際 DOM 更新。

完整範例,展示批量更新與微任務/宏任務的互動:

import React, { useState } from "react";

function App() {
const [count, setCount] = useState(0);

const handleClick = () => {
console.log("1. 開始點擊事件");

// 多次調用 setCount,React 會批量處理
setCount(count + 1);
setCount(count + 2);
console.log("2. 調用 setCount 後,當前 count:", count); // 仍是舊值

// 模擬微任務
Promise.resolve().then(() => {
console.log("3. 微任務執行,當前 count:", count); // 狀態尚未更新
});

// 模擬宏任務
setTimeout(() => {
console.log("4. 宏任務執行,當前 count:", count); // 閉包捕獲舊值
}, 0);

console.log("5. 點擊事件結束");
};

console.log("6. 渲染,當前 count:", count);

return (
<div>
<h1>計數器: {count}</h1>
<button onClick={handleClick}>加數值</button>
</div>
);
}

export default App;

預期 Console 輸出

6. 渲染,當前 count: 0
1. 開始點擊事件
2. 調用 setCount 後,當前 count: 0
5. 點擊事件結束
3. 微任務執行,當前 count: 0
6. 渲染,當前 count: 2
4. 宏任務執行,當前 count: 0

操作步驟

  1. 在專案中貼上程式碼,重啟 npm start。

  2. 點擊按鈕,觀察輸出順序:微任務(3)在宏任務(4)前執行,渲染(6)發生在微任務階段,count 批量更新為 2。

React 18 的自動批量更新範例

以下範例展示非同步操作中的批量處理(仍為微任務):

import React, { useState } from "react";

function App() {
const [count, setCount] = useState(0);

const handleClick = () => {
console.log("1. 開始點擊事件");

setTimeout(() => {
console.log("2. setTimeout 開始");
setCount(count + 1);
setCount(count + 2);
console.log("3. setTimeout 內,調用 setCount 後,當前 count:", count);

Promise.resolve().then(() => {
console.log("4. 微任務執行,當前 count:", count);
});
}, 0);

console.log("5. 點擊事件結束");
};

console.log("6. 渲染,當前 count:", count);

return (
<div>
<h1>計數器: {count}</h1>
<button onClick={handleClick}>非同步加數值</button>
</div>
);
}

export default App;

預期輸出

6. 渲染,當前 count: 0
1. 開始點擊事件
5. 點擊事件結束
2. setTimeout 開始
3. setTimeout 內,調用 setCount 後,當前 count: 0
4. 微任務執行,當前 count: 0
6. 渲染,當前 count: 2

操作步驟:同上,替換程式碼並點擊測試。注意:setTimeout 內的更新仍被批量處理為微任務。

React 的非同步操作:與 Event Loop 協調

React 元件常使用 fetch 或 setTimeout,這些操作將回調推入 Task Queue 或 Microtask Queue,然後觸發狀態更新(微任務)。

完整範例,使用 useEffect 處理非同步資料載入:

jsx

import React, { useState, useEffect } from "react";

function DataFetcher() {
const [data, setData] = useState(null);

useEffect(() => {
console.log("開始 fetch 資料");
fetch("https://jsonplaceholder.typicode.com/posts/1") // 使用公開 API 測試
.then((response) => response.json())
.then((result) => {
setData(result.title); // 觸發狀態更新(微任務)
console.log("資料已設定:", result.title);
});
}, []);

return <div>{data ? <p>資料: {data}</p> : <p>載入中...</p>}</div>;
}

export default DataFetcher;

運作流程

  1. useEffect 同步執行,fetch 推入 Web API。

  2. 請求完成後,.then 推入 Microtask Queue。

  3. 微任務執行 setData,React 排入內部隊列,觸發渲染。

操作步驟

  1. 貼上程式碼到 src/App.js(或整合到 App 元件)。

  2. 執行專案,觀察 Console:fetch 回調在 Microtask Queue,狀態更新後渲染新資料。

總結:React 如何優化 Event Loop

  • 事件處理:依賴宏任務(Task Queue),使用合成事件確保一致性。

  • 狀態更新:作為微任務(Microtask Queue)批量處理,React 18 擴展到非同步場景,提升性能。

  • 非同步操作:回調進入 Task/Microtask Queue,協調狀態更新與渲染。