跳到主要内容

批处理

在 React 函数组件中,当多个 useState 的状态同时更新时,React 并不会为每个状态变化分别触发一次重新渲染,而是将它们“批处理”(batching)成一次重新渲染


✅ 一、核心机制:自动批处理(Automatic Batching)

📌 React 18+ 的关键改进

React 18 开始,无论状态更新发生在什么上下文(事件处理器、Promise、setTimeout 等),所有状态更新都会被自动批处理(除非显式 opt-out)。

🔔 在 React 17 及之前,只有 React 事件处理器内部 的更新会被批处理;在 setTimeoutPromise、原生事件中则不会。


🧪 示例:多个 setState 同时调用

import { useState } from 'react';

export default function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

const handleClick = () => {
console.log('--- 开始更新 ---');
setCount(c => c + 1); // 更新 1
setFlag(f => !f); // 更新 2
console.log('--- 更新结束 ---');
};

console.log('App rendered'); // 观察渲染次数

return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag ? 'ON' : 'OFF'}</p>
<button onClick={handleClick}>Update Both</button>
</div>
);
}

🔍 输出结果(React 18+):

--- 开始更新 ---
--- 更新结束 ---
App rendered // ← 只打印一次!

结论:尽管调用了两次 setState,但组件只重新渲染了一次


✅ 二、底层原理:Fiber 与 Update Queue

React 内部使用 Fiber 架构管理更新:

  1. 每次调用 setXXX,会创建一个 update 对象,并推入该 hook 的 update queue
  2. React 会启动一个 reconciliation(协调)过程
  3. 在这个过程中,React 合并(merge)同一轮事件中的所有状态更新
  4. 最终,用最新的状态值执行一次完整的函数组件重运行(render)。

💡 所以,函数组件的“刷新” = 整个函数重新执行一次,使用所有 state 的最新值。


✅ 三、不同上下文下的批处理行为(React 18+)

上下文是否批处理?说明
React 事件处理器(如 onClick✅ 是原生支持
setTimeout / setInterval✅ 是(React 18+)自动批处理
Promise.then / async/await✅ 是(React 18+)自动批处理
原生 DOM 事件(如 addEventListener✅ 是(React 18+)自动批处理
ReactDOM.flushSync(() => { ... })❌ 否强制同步更新,不批处理

🧪 示例:在 setTimeout 中更新多个状态

const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 0);
};
// → 仍然只触发一次 re-render(React 18+)

✅ 四、特殊情况:如何“退出”批处理?

如果你真的需要让每次 setState 都触发独立渲染(极少见),可以用 flushSync

import { flushSync } from 'react-dom';

const handleClick = () => {
flushSync(() => {
setCount(c => c + 1); // 立即触发一次 render
});
flushSync(() => {
setFlag(f => !f); // 再触发一次 render
});
};

⚠️ 警告:这会显著降低性能,通常用于需要立即读取 DOM 布局的场景(如动画)。


✅ 五、常见误区澄清

❌ 误区 1:“每个 setState 都会触发一次 render”

✅ 正确:React 会合并同一轮更新中的所有 setState,只 render 一次。

❌ 误区 2:“useState 是独立的,所以会分别更新”

✅ 正确:虽然每个 useState 有独立的状态和 setter,但 React 调度器会统一处理本轮所有更新

❌ 误区 3:“useReducer 不会批处理”

✅ 正确:useReducer 的 dispatch 同样参与批处理!

dispatch({ type: 'inc' });
dispatch({ type: 'toggle' });
// → 只 render 一次

✅ 总结

问题答案
多个 useState 同时更新,会 render 几次?1 次(React 18+ 自动批处理)
渲染时用的状态值是什么?所有状态的最新值
这个机制叫什么?Automatic Batching(自动批处理)
能否关闭批处理?可以,用 flushSync,但不推荐
useReducer 是否同样批处理?✅ 是

批处理正是这一理念的体现:无论你调用多少次 setState,React 只关心“最后的状态”,并高效地渲染一次。