人人都会AI编程

11.2 自动批处理:同步异步场景统一批量更新

更新时间:2026-07-11

在 React 中,批处理是指将多次状态更新合并为一次重渲染,以减少不必要的渲染次数,提升性能。React 18 之前,批处理只在 合成事件生命周期钩子 中自动生效,而在 setTimeoutPromise、原生事件等异步回调中则不会合并,导致额外的渲染。React 18 引入自动批处理,统一了所有场景下的更新合并行为。

批处理的核心意义

当你在同一逻辑块中连续调用多个 setState(或 useStatesetCount)时,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 托管的事件处理程序(如 onClickonChange)和 生命周期方法。一旦离开 React 的“安全区”,进入异步回调(setTimeoutPromise.thenfetch 回调、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 会牺牲性能,应谨慎使用,仅在确实无法用其他方式实现时使用。

实际开发中的影响

  1. 性能提升零成本:你无需手动用 ReactDOM.unstable_batchedUpdates 包裹异步更新,代码更干净。
  2. 行为一致性:不再需要担心“为什么在 setTimeout 里多渲染了一次”,降低了认知负担。
  3. 旧代码无缝兼容:如果你的应用升级到 React 18 并使用 createRoot,大部分代码无需修改即可享受自动批处理。
  4. 注意点:某些依赖立即读取最新状态的逻辑可能需要调整,例如在异步更新后立即依赖旧状态的代码,不过这类场景本身就应使用函数式更新(setState(prev => ...))来避免闭包陷阱。

底层原理简述

React 在内部使用 调度器优先级 来控制渲染,批处理的关键在于:React 18 将状态更新的调度推迟到微任务后(或浏览器空闲时),并在提交前合并所有同优先级的更新。自动批处理使得即使更新发源于不同的上下文(setTimeoutPromise),最终也会被相同的调度机制捕获并合并。

简言之,React 18 让“只要在同一个事件循环内触发的更新,React 就能智能合并”成为现实