前端框架

-

batch update 是什麼?React 非同步的狀態更新。

this.web

batch update 是什麼?文章封面

在 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)。

直到 4 才會 console.log

要解決這個問題很簡單,用 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>
	);
}
用 useEffect 解決問題

為什麼 React 要把狀態更新搞得這麼麻煩呢?這就要介紹到今天的主角 batch update。

batch update 批量狀態更新是什麼?

React 的 batches updates 其實是一種優化功能,將多個狀態更改組合成一個更新,然後一次性重新渲染組件這有助於減少重新渲染次數,從而提升應用程式的速度和效能。這種批量更新是自動的,適用於事件處理程序、PromisessetTimeout 等各種場景。

所以如果我們在一個事件處理中,連續多次使用 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 會把狀態更新收集起來,一次更新

當事件處理結束後,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 的更新邏輯會變成:

  1. 從佇列中取出第一個更新函數,執行 prev => prev + 1,將 count 從 0 更新為 1。
  2. 從佇列中取出第二個更新函數,執行 prev => prev + 1,將 count 從 1 更新為 2。
  3. 從佇列中取出第三個更新函數,執行 prev => prev + 1,將 count 從 2 更新為 3。
  4. 處理完所有更新後,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>
	);
}

他的更新邏輯是:

  1. 執行 prev => prev + 1,將 count 從 0 更新為 1。
  2. 執行 setCount(32),將 count 更新成 32
  3. 執行 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 不進行批次更新,點擊按鈕後:

  1. setCount(…) 會觸發一次 re-render。
  2. 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 事件處理(如 onClickonChange 等)中調用的狀態更新才會被批次更新。如果你在 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。最好的方法是使用 useEffectuseLayoutEffect 來處理。

而 React 18 的自動批次更新也進一步擴展了這功能的適用範圍。讓我們更不必擔心 re-render 的效能問題。

希望這篇文章能幫助你更好地理解 React 的批次狀態更新,並在日常開發中靈活應用這一功能!

你可能會感興趣的文章 👇