React 批量更新(Batching)
多次 state 更新为什么通常只触发一次重新渲染?
先记住一句话:批量更新是 React 将同一批次中的多个更新集中处理,尽量减少 Render 和 Commit 次数的机制;它不会把多个 state 合并成一个值,也不会立即修改当前渲染中的变量。
1. 什么是批量更新?
React 通常会把这两个更新放在同一批次中处理,最后再进行一次组件树更新。这样可以避免出现短暂的中间 UI,例如先更新了 count、下一次才更新 flag。
批量更新主要减少的是:
- 组件函数执行次数;
- Render 次数;
- Commit 到 DOM 的次数。
它不会改变更新队列的顺序。多个更新仍然会按照 React 规定的顺序计算。
2. 批量更新和状态快照
批量更新不代表 setter 会立即修改当前变量:
如果连续更新依赖旧状态,使用函数式更新:
直接传值时,两次调用可能都基于同一个旧快照:
这和批量更新是两个不同概念:前者是更新如何计算,后者是 React 何时集中处理这些更新。
3. React 17 及以前
在传统 React 17 渲染方式下,React 主要会在自己的事件处理流程中自动批量更新:
但在 setTimeout、Promise 回调和原生事件监听器等 React 控制范围之外的场景,更新通常不会自动批量:
这描述的是 React 17 的典型行为。具体结果还会受到渲染入口、版本和手动批处理 API 的影响。
4. React 18 的自动批量更新
使用 createRoot 创建的 React 18 应用,会把自动批处理扩展到更多场景:
- React 事件;
- Promise 回调;
setTimeout/setInterval;- 原生事件监听器;
- 其他由这些任务触发的 state 更新。
需要注意:如果应用仍使用旧的 ReactDOM.render 入口,React 18 可以保留旧的更新行为以便迁移。是否启用完整的 React 18 自动批处理,首先要确认应用是否使用 createRoot。
5. 批量更新不等于“合并 state”
批量更新和类组件的对象 state 合并不是一回事:
函数组件中的 useState setter 会用新值替换旧状态,不会自动浅合并对象。
6. 为什么批量更新有用?
减少重复渲染
如果一个交互需要更新多个相关状态,集中处理可以减少组件树重复计算。
避免中间状态
批量处理让用户通常直接看到完整的一组状态变化,而不是先看到某个字段已经更新、其他字段还没更新的中间画面。
保持更新顺序
批量不是简单丢弃重复调用。React 仍会处理更新队列:
从 1 开始时,结果是 4,因为更新按队列顺序计算:
7. 什么时候使用 flushSync?
绝大多数业务代码不需要退出批处理。只有在必须在同一段代码中立刻读取更新后的 DOM 时,才考虑 flushSync:
flushSync 会强制 React 同步完成更新,并可能影响性能或触发额外的 effect。不要把它当成“让 state 立即更新”的常规 API,也不要在普通状态流转中滥用。
8. 常见误区
批量更新一定只渲染一次
不应把“一次批次”当成绝对的一次组件执行。更新可能被拆分、优先级不同,或者因为其他同步工作产生额外渲染。批处理的目标是减少不必要的工作,而不是承诺固定次数。
setState 调用后立即读取变量能拿到新值
拿到的仍然是当前渲染的状态快照。需要使用最新状态时,通过下一次渲染、effect 或函数式 updater 处理。
批处理会深度合并对象
不会。useState 的新对象会替换旧对象,需要手动展开合并。
所有性能问题都能靠批处理解决
不能。昂贵的计算、大列表、过大的组件更新范围和首屏包体仍需要通过状态下沉、列表虚拟化、代码分割和性能分析解决。
9. 与相关机制的关系
总结
批量更新可以概括为:
- React 将同一批次中的多个更新集中处理;
- React 18 配合
createRoot后,更多异步场景默认支持自动批量; - 批量更新不改变状态快照,也不等于对象合并;
- 依赖旧状态时使用函数式更新;
- 只有需要立即读取 DOM 时,才谨慎使用
flushSync。
面试时可以这样回答:Batching 是 React 减少重复 Render 和 Commit 的机制。React 17 主要在 React 事件中批量,React 18 使用 createRoot 后扩展到 Promise、定时器和原生事件等场景。批量更新不会让 setter 同步修改当前 state,也不会自动合并对象;需要基于旧值连续更新时应使用函数式写法。

