Skip to main content

深入理解 batch update 與 updater function

Batch Update (批次更新)

什麼是 Batch Update?

在 React 中,batch update(批次更新)指的是 React 會將多個狀態更新操作「收集」起來,然後在某個時間點一次性地應用這些更新,而不是每次狀態改變就立即重新渲染畫面。這種機制可以有效減少不必要的渲染,提升應用程式的效能。

React 的批次更新主要發生在以下情境:

  • 事件處理函數(如 onClick、onChange 等)內的多個 setState 呼叫。

  • React 的事件處理系統(Synthetic Event System)會自動將這些更新「批次處理」。

  • 在非事件處理的情境(例如 setTimeout 或 Promise 中),React 可能不會自動批次處理,這時需要特別注意。

為什麼需要 Batch Update?

如果 React 每次呼叫 setState 都立即重新渲染,當有多個狀態更新時,會導致多次不必要的 DOM 操作,影響效能。批次更新讓 React 能:

  1. 將多次 setState 合併為單一更新。

  2. 減少重新渲染次數。

  3. 確保 UI 一致性,避免中間狀態的渲染。

範例:Batch Update 的行為

以下是一個簡單的 React 範例,展示批次更新如何運作:

import React, { useState } from "react";

function BatchUpdateExample() {
const [count, setCount] = useState(0);
const [name, setName] = useState("小明");

const handleClick = () => {
// 連續呼叫多個 setState
setCount(count + 1);
setCount(count + 2);
setName("小華");
console.log("當前 count:", count); // 注意:這裡的 count 仍是舊值
};

console.log("渲染時的 count:", count, "name:", name);

return (
<div>
<h1>計數: {count}</h1>
<h2>名字: {name}</h2>
<button onClick={handleClick}>點擊更新</button>
</div>
);
}

export default BatchUpdateExample;

步驟解析:

  1. 初始化狀態

    • 使用 useState 建立兩個狀態:count(初始為 0)和 name(初始為 '小明')。
  2. 事件處理

    • 在 handleClick 中,連續呼叫 setCount 兩次和 setName 一次。

    • 雖然有多次 setState,React 會將這些更新「批次處理」,只在事件處理結束後進行一次重新渲染。

  3. 觀察結果

    • 點擊按鈕後,count 只會更新到 2(因為 setCount(count + 2) 覆蓋了前面的 setCount(count + 1))。

    • name 會更新為 '小華'。

    • 在 handleClick 內的 console.log 顯示的 count 仍是舊值,因為狀態更新是「非同步」的,React 尚未應用新值。

重要提醒:

  • 批次更新只在 React 事件處理中自動發生。如果在 setTimeout 或 Promise 中呼叫 setState,React 不會自動批次處理,可能導致多次渲染。

非批次更新的範例:

以下展示在 setTimeout 中不進行批次更新的情況:

import React, { useState } from "react";

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

const handleClick = () => {
setTimeout(() => {
setCount(count + 1);
setCount(count + 2);
console.log("setTimeout 中的 count:", count);
}, 1000);
};

console.log("渲染時的 count:", count);

return (
<div>
<h1>計數: {count}</h1>
<button onClick={handleClick}>延遲更新</button>
</div>
);
}

export default NonBatchUpdateExample;

結果解析:

  • 點擊按鈕後,setTimeout 會延遲 1 秒執行。

  • 在 setTimeout 中,setCount(count + 1)setCount(count + 2) 不會被批次處理,導致 count 最終只更新到 2,且可能觸發多次渲染。

  • 這是因為 setTimeout 不在 React 的事件處理系統中。


Updater Function (更新函數)

什麼是 Updater Function?

在 React 中,setState(或 useState 的 setter 函數)可以接受一個函數作為參數,這個函數稱為 updater function(更新函數)。它用來根據先前的狀態計算新的狀態,特別適用於需要依賴當前狀態進行更新的場景。

語法

setState((prevState) => {
return newState;
});

為什麼需要 Updater Function?

  1. 避免狀態競爭問題

    • 在批次更新或非同步操作中,直接使用當前的狀態值(例如 count)可能導致結果不正確,因為 count 可能尚未更新。

    • Updater function 保證你拿到的是最新的狀態值(prevState)。

  2. 更可靠的狀態更新

    • 使用 updater function,React 會將最新的狀態傳入,讓你基於這個值計算新狀態。
  3. 提升程式碼可讀性

    • 明確表示新狀態依賴於舊狀態,邏輯更清晰。

範例:使用 Updater Function

以下範例展示如何使用 updater function 來正確更新狀態:

import React, { useState } from "react";

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

const handleClick = () => {
// 使用 updater function
setCount((prevCount) => {
console.log("前一個 count:", prevCount);
return prevCount + 1;
});
setCount((prevCount) => {
console.log("前一個 count:", prevCount);
return prevCount + 2;
});
};

console.log("渲染時的 count:", count);

return (
<div>
<h1>計數: {count}</h1>
<button onClick={handleClick}>點擊更新</button>
</div>
);
}

export default UpdaterFunctionExample;

步驟解析:

  1. 定義 updater function

    • setCount 接受一個函數,該函數接收 prevCount(前一個狀態)並返回新狀態。
  2. 連續更新

    • 第一次 setCount 接收 prevCount(假設為 0),返回 0 + 1 = 1。

    • 第二次 setCount 接收 prevCount(現在為 1),返回 1 + 2 = 3。

  3. 結果

    • 點擊按鈕後,count 最終為 3,因為 updater function 確保每次更新都基於最新的狀態。

對比不使用 Updater Function:

如果直接使用 setCount(count + 1),由於批次更新,count 可能不會正確累加(如同前述範例中的問題)。

在非批次更新情境中使用 Updater Function

在 setTimeout 或其他非 React 事件的情境中,updater function 特別重要,因為它能確保正確的狀態更新:

import React, { useState } from "react";

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

const handleClick = () => {
setTimeout(() => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 2);
console.log("setTimeout 中的 count:", count);
}, 1000);
};

console.log("渲染時的 count:", count);

return (
<div>
<h1>計數: {count}</h1>
<button onClick={handleClick}>延遲更新</button>
</div>
);
}

export default UpdaterInTimeoutExample;

結果解析:

  • 在 setTimeout 中使用 updater function,確保 setCount 基於最新的 prevCount 進行更新。

  • 點擊按鈕後,1 秒後 count 會從 0 變為 3(0 → 1 → 3)。


總結與實作建議

Batch Update 要點:

  • React 會在事件處理函數中自動批次處理多個 setState。

  • 在非事件處理情境(如 setTimeout、Promise),需要特別注意,可能需要手動控制更新邏輯。

  • 批次更新減少了不必要的渲染,提升效能。

Updater Function 要點:

  • 使用 updater function(prevState => newState)確保狀態更新的正確性。

  • 特別適用於連續更新或非同步情境。

  • 提高程式碼的可讀性和可靠性。

實作建議:

  1. 優先使用 Updater Function

    • 當狀態更新依賴於前一個狀態時,總是使用 updater function。
  2. 檢查非同步情境

    • 如果在 setTimeout、Promise 或其他非 React 事件中更新狀態,務必使用 updater function。
  3. 測試與除錯

    • 使用 console.log 檢查狀態更新順序,確保符合預期。

    • 在複雜應用中,可以使用 React DevTools 檢查渲染次數和狀態變化。

完整範例應用

以下是一個結合 batch update 和 updater function 的完整範例,模擬一個簡單的計數器應用:

import React, { useState } from "react";

function CounterApp() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("尚未更新");

const handleIncrement = () => {
// 批次更新 + updater function
setCount((prevCount) => {
console.log("前一個 count:", prevCount);
return prevCount + 1;
});
setCount((prevCount) => {
console.log("前一個 count:", prevCount);
return prevCount + 2;
});
setMessage("計數已增加");
};

const handleDelayedIncrement = () => {
setTimeout(() => {
setCount((prevCount) => prevCount + 1);
setMessage("延遲更新完成");
}, 1000);
};

return (
<div>
<h1>計數: {count}</h1>
<h2>訊息: {message}</h2>
<button onClick={handleIncrement}>立即增加</button>
<button onClick={handleDelayedIncrement}>延遲增加</button>
</div>
);
}

export default CounterApp;

操作步驟:

  1. 將此程式碼複製到你的 React 專案中(例如 src/CounterApp.js)。

  2. 在 App.js 中引入並使用:

    import CounterApp from "./CounterApp";

    function App() {
    return (
    <div>
    <CounterApp />
    </div>
    );
    }

    export default App;
  3. 啟動專案(npm start),打開瀏覽器觀察結果。

  4. 點擊「立即增加」按鈕,觀察 count 和 message 的變化。

  5. 點擊「延遲增加」按鈕,等待 1 秒後觀察變化。