在 React 中,批处理是指将多次状态更新合并为一次重渲染,以减少不必要的渲染次数,提升性能。React 18 之前,批处理只在 合成事件 和 生命周期钩子 中自动生效,而在 setTimeout、Promise、原生事件等异步回调中则不会合并,导致额外的渲染。React 18 引入自动批处理,统一了所有场景下的更新合并行为。
批处理的核心意义
当你在同一逻辑块中连续调用多个 setState(或 useState 的 setCount)时,React 不会立即触发重渲染,而是将更新收集起来,等到逻辑块执行完毕后,统一计算最终状态并执行一次渲染。这避免了中间状态的无效渲染,例如:
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 不会在此处渲染两次,而是合并为一次
}
React 17 的批处理局限
React 17 及以前,批处理仅适用于 React 托管的事件处理程序(如 onClick、onChange)和 生命周期方法。一旦离开 React 的“安全区”,进入异步回调(setTimeout、Promise.then、fetch 回调、await 后代码)或原生事件监听器中,状态更新就会失去批处理能力,每次 setState 都会触发一次独立渲染。
React 17 行为示例:
function handleClick() {
// ✅ 合成事件内:两次更新合并为一次渲染
setCount(c => c + 1);
setFlag(f => !f);
}
function handleAsync() {
setTimeout(() => {
// ❌ 异步回调内:每次 setState 触发一次单独渲染
setCount(c => c + 1); // 渲染一次
setFlag(f => !f); // 再渲染一次
}, 1000);
}
这种不一致的行为很容易引发性能问题,且开发者可能无意识地在异步操作中写入连续更新而导致重复渲染。
React 18 自动批处理统一行为
从 React 18 开始,通过 createRoot 渲染的应用,所有状态更新(无论在何处触发)都会自动批处理。无论是合成事件、异步回调、原生事件还是 useEffect 清理函数,React 都能智能地合并同一事件循环中的多次更新。
React 18 行为示例:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
// 合成事件:自动批处理(与 v17 一致,但内部机制强化)
const handleClick = () => {
setCount(c => c + 1);
setFlag(f => !f);
// 仅触发一次渲染
};
// 异步回调:现在也自动批处理
const handleAsync = () => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 同样仅触发一次渲染 ✅
}, 1000);
};
// 原生事件:也自动批处理
useEffect(() => {
const el = document.getElementById('native-btn');
const handler = () => {
setCount(c => c + 1);
setFlag(f => !f);
// 仅一次渲染 ✅
};
el.addEventListener('click', handler);
return () => el.removeEventListener('click', handler);
}, []);
console.log('渲染次数'); // 帮助验证
return (
<div>
<button onClick={handleClick}>合成事件</button>
<button onClick={handleAsync}>异步更新</button>
<button id="native-btn">原生事件</button>
</div>
);
}
无论哪种触发方式,同一调用栈内的连续 setState 都被合并为一次渲染。
如何选择退出自动批处理?
极少数场景下,你可能希望立即获取更新后的 DOM 或强制同步渲染(例如在测量布局前后立即读/写 DOM),此时可以使用 ReactDOM.flushSync() 包裹更新,退出批处理。
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCount(c => c + 1); // 立即提交,触发同步重渲染
});
// 此时 DOM 已更新,可以读取新布局
const height = document.getElementById('box').offsetHeight;
flushSync(() => {
setFlag(true);
});
}
注意:flushSync 会牺牲性能,应谨慎使用,仅在确实无法用其他方式实现时使用。
实际开发中的影响
- 性能提升零成本:你无需手动用
ReactDOM.unstable_batchedUpdates包裹异步更新,代码更干净。 - 行为一致性:不再需要担心“为什么在
setTimeout里多渲染了一次”,降低了认知负担。 - 旧代码无缝兼容:如果你的应用升级到 React 18 并使用
createRoot,大部分代码无需修改即可享受自动批处理。 - 注意点:某些依赖立即读取最新状态的逻辑可能需要调整,例如在异步更新后立即依赖旧状态的代码,不过这类场景本身就应使用函数式更新(
setState(prev => ...))来避免闭包陷阱。
底层原理简述
React 在内部使用 调度器 和 优先级 来控制渲染,批处理的关键在于:React 18 将状态更新的调度推迟到微任务后(或浏览器空闲时),并在提交前合并所有同优先级的更新。自动批处理使得即使更新发源于不同的上下文(setTimeout、Promise),最终也会被相同的调度机制捕获并合并。
简言之,React 18 让“只要在同一个事件循环内触发的更新,React 就能智能合并”成为现实。