前端知库

React 的批量更新是什么?React 18 的自动批处理改变了什么?

React 不会每调用一次 setState 就重新渲染,而是等当前事件里的代码跑完,把这期间的所有更新合并成一次渲染。React 18 之前只在 React 事件处理函数里合并,setTimeout、Promise 里每次 setState 都单独渲染;React 18 用 createRoot 后,所有地方的更新都会自动合并。需要立刻更新 DOM 时可以用 flushSync。

参考答案

简答

一个点击事件里连续调用了 setCount 和 setFlag,React 不会渲染两次,而是等这个事件处理函数里的代码全部跑完,再把两次更新一起处理,只渲染一次。这就是批量更新,也叫批处理。React 文档打了个比方:服务员会等你点完所有菜再去厨房,而不是听到一个菜名就跑一趟。

这样做是为了少做无用功。如果每次 setState 都立即渲染,中间那些只更新了一半的状态也会被渲染出来,既浪费又可能让界面闪出不一致的样子。

React 18 改变的是批处理的范围。之前只有 React 事件处理函数里的更新会合并,放在 setTimeout、Promise 回调、原生事件监听里的更新,每调用一次就渲染一次。React 18 用 createRoot 渲染后,无论更新从哪里来,都会自动合并,这叫自动批处理。

要注意,批处理意味着调用 setState 后,当前函数里读到的状态还是旧值,DOM 也还没更新。确实需要马上拿到更新后的 DOM,比如更新列表后立刻滚动到新的一项,可以把更新包在 flushSync 里,但它会强制同步渲染,影响性能,只在必要时使用。

React 18 前后的区别

下面的代码在 setTimeout 里连续更新两个状态:

JSX
function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  console.log('渲染');

  function handleClick() {
    setTimeout(() => {
      setCount((c) => c + 1);
      setFlag((f) => !f);
    }, 0);
  }

  return <button onClick={handleClick}>{count}</button>;
}

点一次按钮,React 17 会打印两次“渲染”,React 18 用 createRoot 时只打印一次。

为什么 React 17 在这里不合并?因为它的批处理是靠“正在执行 React 事件处理函数”这个标记实现的。setTimeout 的回调在事件处理函数执行完之后才运行,那时标记已经清掉了,React 不知道这两次更新属于同一批,只好各渲染一次。

React 18 换成了按时间合并:同一个任务里产生的更新,先放进队列,等当前代码跑完再统一处理,不再关心它是不是来自 React 事件。所以 setTimeout、Promise、原生事件里的更新也都合并了。

要注意,如果项目还在用旧的 ReactDOM.render,即使升级到 React 18,也保持旧的行为,只有改用 createRoot 才会启用自动批处理。

需要立即更新时用 flushSync

JSX
import { flushSync } from 'react-dom';

function handleAdd() {
  flushSync(() => {
    setItems((items) => [...items, newItem]);
  });
  listRef.current.lastChild.scrollIntoView();
}

如果不用 flushSync,scrollIntoView 执行时新的一项还没渲染到 DOM 里,滚动到的是原来的最后一项。包在 flushSync 里,React 会立刻同步渲染完,下一行就能拿到新的 DOM。

常见追问

为什么 setState 之后马上读 state 还是旧值?

一是批处理,更新还在队列里没处理;二是函数组件里的 count 是这一次渲染时的常量,就算渲染完了,当前这次调用里的变量也不会变。详见《为什么调用 setState 后,当前变量还是旧值?》。

连续调用三次 setCount(count + 1),结果为什么只加了 1?

三次调用读到的都是同一个 count,比如都是 0,所以三次都是“设成 1”,合并后结果还是 1。想要每次都基于最新值累加,用函数式更新 setCount((c) => c + 1),React 会把队列里的函数依次执行,结果是 3。

能不能关掉自动批处理?

没有全局开关。需要某一处不合并时,用 flushSync 包住那次更新。