前端框架
-batch update 是什麼?React 非同步的狀態更新。
this.web

在 React 裡,當 state 改變時,React 並不會馬上 re-render 整個組件,而是會先搜集要更新的 state,將這些更新的 state 放到一個佇列(queue)中,等待適當的時機再一次進行處理。
我們來看以下的例子,我希望在 count 等於 3 時,執行某些功能,所以我們可能寫出這樣的程式碼:
export default function Count() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
if (count === 3) {
console.log('count is 3');
}
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
但是當你點擊 button 直到 count 等於 3 時,你會發現他沒有如我們預期的顯示 console.log
。就像上面所說,state 更新時 react 並不會馬上更新組件,狀態更新在 react 中是非同步的,在 setCount
執行時,count
的值仍然是舊值(4)。

要解決這個問題很簡單,用 useEffect
就好了,這也很符合他的用法 -- 副作用。我們用 useEffect
監聽 count
,當 count 等於 3 時去執行邏輯。
export default function Count() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count === 3) {
console.log('count is 3');
}
}, [count])
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}

為什麼 React 要把狀態更新搞得這麼麻煩呢?這就要介紹到今天的主角 batch update。
batch update 批量狀態更新是什麼?
React 的 batches updates 其實是一種優化功能,將多個狀態更改組合成一個更新,然後一次性重新渲染組件。這有助於減少重新渲染次數,從而提升應用程式的速度和效能。這種批量更新是自動的,適用於事件處理程序、Promises
、setTimeout
等各種場景。
所以如果我們在一個事件處理中,連續多次使用 useState
,他也只會 re-render 一次。
export default function BatchesStateUpdates() {
const [count, setCount] = useState(0);
const handleClick = () => {
// state 要更新,先收集起來
setCount(1);
console.log(count) // 0
// state 要更新,先收集起來(還沒有更新)
setCount(2);
console.log(count) // 0
// state 要更新,先收集起來(還沒有更新)
setCount(3);
console.log(count) // 0
// 沒有事情了,來一次更新所有的 states
};
// ...
}
所以對 React 來說,他下次更新(render) 組件時,會直接將 count
設為 3。
這也是為什麼 console.log
出來的結果都是舊的。
batch update 的更新邏輯
讓我們來看一個例子,假設你希望在一次點擊中將 count
增加 3,你可能會這樣寫:
export default function Count() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 基於 count = 0,放入「設為 1」的請求
console.log('count + 1');
setCount(count + 1); // 基於 count = 0,放入「設為 1」的請求
console.log('count + 1');
setCount(count + 1); // 基於 count = 0,放入「設為 1」的請求
console.log('count + 1');
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<p>Count: {count}</p>
</div>
);
}
但程式碼出來的結果是 count = 1
。為什麼呢?因為每次 setCount(count + 1)
都是基於當前的 count
值(0)計算的,React 會將這三個更新請求放入佇列,但每個請求都是「將 count
設為 1」。

當事件處理結束後,React 會處理佇列,最後只應用最新的值,也就是 1。
那我們到底要如何讓 count
增加 3 呢?這時候就可以用到更新函數(updater function)。我們可以改進程式碼如下:
export default function Count() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1); // 放入「基於前值加 1」的請求
setCount(prev => prev + 1); // 放入「基於前值加 1」的請求
setCount(prev => prev + 1); // 放入「基於前值加 1」的請求
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<p>Count: {count}</p>
</div>
);
}
如果我們傳入 function 給 setCount ,那 React 的更新邏輯會變成:
- 從佇列中取出第一個更新函數,執行
prev => prev + 1
,將count
從 0 更新為 1。 - 從佇列中取出第二個更新函數,執行
prev => prev + 1
,將count
從 1 更新為 2。 - 從佇列中取出第三個更新函數,執行
prev => prev + 1
,將count
從 2 更新為 3。 - 處理完所有更新後,React 將最終結果 3 儲存為新的狀態值,並觸發一次重新渲染。
再來看看下面這個例子,你覺得值會是多少呢?
export default function Count() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
setCount(32);
setCount(prev => prev + 3);
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<p>Count: {count}</p>
</div>
);
}
他的更新邏輯是:
- 執行
prev => prev + 1
,將count
從 0 更新為 1。 - 執行
setCount(32)
,將count
更新成 32 - 執行
prev ⇒ prev + 3
,將count
從 32 更新為 35
所以最後的值會是 35,你有答對嗎?
為什麼要 batch update?
React 會進行批次更新 (Batching Updates),最主要是為了提升效能和避免不必要的 re-render。這樣可以讓應用程式運行得更快、更流暢。
如果 React 每次 state 變化都立刻 re-render,那麼當你一次更新多個 state 時,React 會觸發多次 re-render,導致效能下降。例如:
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
function handleClick() {
setCount((prev) => prev + 1);
setText("Updated");
}
return <button onClick={handleClick}>{count} - {text}</button>;
}
如果 React 不進行批次更新,點擊按鈕後:
setCount(…)
會觸發一次 re-render。setText(…)
又會觸發一次 re-render。
這樣會讓組件重新渲染 兩次,但其實我們只需要它最後的結果,所以 React 才會試著把這些 state 變更放到 queue 裡,一次性更新,減少 re-render 次數。
我不想要 batch update 怎麼辦?使用 flushSync
雖然批次更新是一個重要的性能優化功能,但有時候你可能需要立即更新狀態並觸發重新渲染,而不是等待批次處理完成。例如,你可能需要在更新狀態後立即操作 DOM,或確保某些邏輯在渲染完成後立即執行。這時,我們就可以使用 React 提供的 flushSync
API。
flushSync
會強制 React 跳過批次處理,同步更新狀態並立即觸發重新渲染。以下是一個使用 flushSync
的例子:
export default function BatchesStateUpdates() {
const [count, setCount] = useState(0);
const handleClick = () => {
flushSync(() => {
setCount(count + 1);
});
const countElement = document.getElementById('count');
if (countElement) {
console.log(countElement.textContent); // 此時 DOM 已經更新,
}
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<p id="count">Count: {count}</p>
</div>
);
}
但要注意的是:
- 使用
flushSync
會跳過 React 的批次處理,強制同步更新,這可能會導致性能問題。官方也建議僅在必要時使用。 - 如果你在同一個事件處理程序中多次使用
flushSync
,每次調用都會觸發一次重新渲染,這可能會非常影響性能。
React 18 後全面支持 batch update
在 React 18 之前,React 的批次更新功能有一些限制。例如,只有在 React 事件處理(如 onClick
、onChange
等)中調用的狀態更新才會被批次更新。如果你在 setTimeout
、Promise 或其他異步操作中更新狀態,React 就不會進行批次更新,可能導致多次重新渲染。
以下拿一個 React 17 中的例子:
export default function BatchesStateUpdates() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const handleClick = () => {
setTimeout(() => {
setCount(count + 1); // 不會被批次更新
setText('Updated'); // 不會被批次更新
}, 1000);
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<p>Count: {count}</p>
<p>Text: {text}</p>
</div>
);
}
在 React 17 中,setTimeout
內的兩個 setState
調用會導致組件重新渲染兩次,這降低了性能。
文章總結
React 的批次狀態更新(batch update)會將多個狀態更新組合成單次重新渲染,減少了不必要的渲染次數,目的是為了提升應用程式的效能。所以在一次事件或非同步函數中,是沒把法即時取得新的狀態值的,只能透過 flushSync
來做到。不過 React 官方建議除非必要,不然不要用 flushSync
。最好的方法是使用 useEffect
或 useLayoutEffect
來處理。
而 React 18 的自動批次更新也進一步擴展了這功能的適用範圍。讓我們更不必擔心 re-render 的效能問題。
希望這篇文章能幫助你更好地理解 React 的批次狀態更新,並在日常開發中靈活應用這一功能!