什么是批处理
批处理(Batching)指的是 React 将多个状态更新合并为一次重新渲染,从而避免不必要的组件重复计算和 DOM 操作,提升性能。
在 React 18 之前,React 已经能够在合成事件和生命周期钩子中自动进行批处理,但对于原生事件、setTimeout、Promise、异步操作中的状态更新却无法合并,导致多次状态变更触发多次渲染。
React 17 的局限
以下代码在 React 17 中会触发两次渲染:
function handleClick() {
// React 17 在合成事件中会批处理
setCount(c => c + 1);
setFlag(f => !f);
// 两次更新合并为一次渲染 ✅
}
// 但在异步回调中,React 17 不会批处理
function handleAsync() {
fetch('/api').then(() => {
setCount(c => c + 1); // 触发渲染
setFlag(f => !f); // 再次触发渲染 ❌
});
}
这种不一致性会让开发者困惑,也可能导致意外的性能问题。
React 18 的自动批处理
React 18 通过引入 createRoot API 并重构更新机制,实现了所有更新都会自动批处理,无论它们发生在什么上下文:合成事件、原生事件、setTimeout、Promise、async/await 等异步操作。
// React 18 中,以下所有场景都会自动批处理
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
// 场景1: 点击事件(合成事件)
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次渲染 ✅
}
// 场景2: Promise 回调
function handleAsync() {
fetch('/api').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次渲染 ✅
});
}
// 场景3: setTimeout 回调
useEffect(() => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次渲染 ✅
}, 1000);
}, []);
console.log('渲染'); // 在上述所有场景中都只会打印一次
return <div>{count} - {flag.toString()}</div>;
}
自动批处理的原理
在 React 18 中,状态更新会被调度为一个微任务或通过内部的任务队列统一处理。当同一个事件循环内发生多个状态更新时,它们会被收集到一个更新队列,然后由 React 在合适的时机一次性处理,生成最新的状态并触发一次渲染。
这依赖于 React 新的调度系统(基于 Lane 模型的优先级调度),能够在任意上下文中将多个更新“包裹”在同一批次中。
如何退出批处理(执行同步更新)
在极少数场景下,你可能希望在一个状态更新后立即读取并基于新的 DOM 状态执行操作(例如手动聚焦输入框、测量元素尺寸),而批处理会延迟 DOM 更新。此时可以使用 ReactDOM.flushSync() 强制同步更新,但这会打断批处理,应谨慎使用。
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCount(c => c + 1);
});
// 此时 DOM 已经更新,可以读取新的 DOM 状态
flushSync(() => {
setFlag(f => !f);
});
// 两次更新会触发两次同步渲染
}
对现有代码的影响
如果你从 React 17 升级到 React 18,并使用 createRoot 替换 ReactDOM.render,绝大多数时候你会立即享受自动批处理带来的性能提升,并且行为更加一致。只有在极少数依赖立即 DOM 更新的旧代码中,才需要用 flushSync 显式处理,但这种模式本身不推荐使用,应优先考虑用 useEffect 或 useLayoutEffect 来处理副作用。
开发中的注意事项
- 不要依赖旧版的多次渲染行为:如果你在异步操作中手动计算渲染次数,升级后行为会改变。
- 异步操作中的状态更新顺序仍然保持不变:批处理只影响渲染时机,不影响状态更新的执行顺序和合并逻辑。
- 使用
React.StrictMode:在开发环境下,React 18 会双重调用某些函数(如 reducer、setState 更新函数)来帮助发现副作用问题,但这不影响生产环境的批处理行为。
总结:React 18 的自动批处理消除了困扰开发者多年的“同步与异步不一致”问题,让应用默认更高效,而你几乎不需要改动任何代码。