我們深入解析了 Render 階段與 Commit 階段的分工。今天我們要討論 React 處理狀態變更的核心底層:UpdateQueue(狀態更新隊列) 與 Automatic Batching(自動批次更新) 機制
當你在組件中呼叫 setState 時,React 並不會「立刻」更新狀態,而是會在背後建立一個 Update 物件 並掛載到 Fiber 節點的 UpdateQueue 鏈表中。
Update 物件結構
const update = {
lane: lane, // 優先級標記 (Bitmask)
action: action, // newState 或 (prevState) => newState 函數
hasEagerState: false, // 是否可提前計算出 State (Eager State 優化)
eagerState: null,
next: null, // 指向下一個 Update (形成單向環狀鏈表)
};
更新隊列的合併(Base State + Updates = New State)
在 Render 階段走訪到該 Fiber 時,processUpdateQueue 函數會從 baseState 出發,依次遍歷 UpdateQueue 鏈表中的更新:
若更新的 lane 符合當前渲染的優先級,則將其計算並累加到最新狀態。
若優先級不足,該 Update 會被跳過 (Skip),並保留至下一次低優先級渲染時再重新計算。
Batching 是 React 的一種效能優化策略:將多個 setState 操作合併為一次 Render 過程執行,避免多次重複觸發重新渲染。
// 在一個事件處理函數中連續觸發多次 setState
const handleClick = () => {
setCount(c => c + 1);
setFlag(f => !f);
// React 會將這兩次 setState 合併,只觸發一次 Re-render!
};
在 React 17 及更早的版本中,批次更新依賴於 batchedUpdates 函數包裹。React 預設只會在 React 事件監聽器(如 onClick、onChange) 內部自動開啟 Batching。
只要脫離了 React 事件的回調環境,Batching 就會直接失效,導致「每一次 setState 都同步觸發一次 Re-render」:
// React 17 及更早版本
const handleClick = () => {
fetchData().then(() => {
// 位於 Promise / setTimeout 非同步微任務中
setCount(c => c + 1); // 觸發第 1 次 Re-render
setFlag(f => !f); // 觸發第 2 次 Re-render
});
};
React 18 引進了全新的根節點建立 API createRoot,並正式啟用 Automatic Batching。
在 React 18 中,無論 setState 發生在哪裡(包含 Promise、setTimeout、原生 DOM 事件處理器、IntersectionObserver 等),React 都會自動統一進行批次處理!
// React 18 自動批次更新
const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 即使在 setTimeout 內部,React 18 依然將兩者合併,只觸發 1 次 Re-render!
}, 1000);
};
有些極端情況下(例如需要在 setState 後立刻讀取最新的 DOM 幾何尺寸),如果你不希望被自動 Batching,可以使用 React 提供的 flushSync:
import { flushSync } from 'react-dom';
const handleClick = () => {
flushSync(() => {
setCount(c => c + 1); // 立刻觸發 DOM 寫入與渲染
});
// 此時 DOM 已經更新完畢
flushSync(() => {
setFlag(f => !f); // 立刻觸發二次渲染
});
};