批处理
在 React 函数组件中,当多个 useState 的状态同时更新时,React 并不会为每个状态变化分别触发一次重新渲染,而是将它们“批处理”(batching)成一次重新渲染。
✅ 一、核心机制:自动批处理(Automatic Batching)
📌 React 18+ 的关键改进
从 React 18 开始,无论状态更新发生在什么上下文(事件处理器、Promise、setTimeout 等),所有状态更新都会被自动批处理(除非显式 opt-out)。
🔔 在 React 17 及之前,只有 React 事件处理器内部 的更新会被批处理;在
setTimeout、Promise、原生事件中则不会。
🧪 示例:多个 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 架构管理更新:
- 每次调用
setXXX,会创建一个 update 对象,并推入该 hook 的 update queue。 - React 会启动一个 reconciliation(协调)过程。
- 在这个过程中,React 合并(merge)同一轮事件中的所有状态更新。
- 最终,用最新的状态值执行一次完整的函数组件重运行(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 只关心“最后的状态”,并高效地渲染一次。