人人都会AI编程

6.5 同步更新与异步更新的场景辨析

更新时间:2026-07-11

React 中的状态更新(setState / useState 的更新函数)并不是每次都立刻触发重渲染。理解何时更新是异步批量处理、何时是同步立即执行,是避免状态混乱和性能问题的关键。

经典困惑:setState 到底是同步还是异步?

很多开发者在学习 React 时会遇到这样的现象:

function Demo() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // 0,并不是 1
  };

  return <button onClick={handleClick}>点击</button>;
}

点击按钮后,控制台打印的 count 仍然是旧值,仿佛 setCount 是“异步”的。但在另一些场景下,例如 setTimeout 中(React 17 及之前),状态更新又像是“同步”的:

const handleClick = () => {
  setTimeout(() => {
    setCount(count + 1);
    console.log(count); // 在 React 17 中会打印 1,同步更新了
  }, 0);
};

这种差异并非 Bug,而是 React 有意为之的批量更新(Batching)机制

合成事件与生命周期中的异步批量更新

在 React 的合成事件(如 onClickonChange)和生命周期函数(类组件 componentDidMount 等)中,状态更新会进行批量处理。React 会收集一个事件处理函数内所有的 setState 调用,然后合并成一次重渲染。这样做既能避免不必要的重渲染,也能防止界面出现中间状态。

因此,在这些场景中:

  • setState异步的(指不会立刻更新组件状态并重渲染)。
  • 在同一个事件处理函数内,多次 setState 只会产生一次重新渲染。
  • 读取状态不能立刻得到最新值,必须通过更新函数的参数方式或者 useEffect 来捕获变化。
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(c => c + 1);
    setCount(c => c + 1);
    // 最终 count 增加 2,只触发一次渲染
  };

  console.log('渲染', count);
  return <button onClick={handleClick}>count is {count}</button>;
}

React 17 中的同步例外:setTimeout、原生事件等

在 React 17 及之前版本,批量更新仅在合成事件和生命周期中得到保证。脱离这些上下文后,React 会同步地更新状态并立即触发重渲染。典型的场景包括:

  • setTimeoutsetInterval 回调
  • 原生 DOM 事件回调(addEventListener
  • Promise.then / async/await
// React 17 行为
function Demo() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      setCount(c => c + 1);
      setCount(c => c + 1);
      // 每次 setCount 都会直接触发一次渲染,渲染两次
    }, 0);
  };

  return <button onClick={handleClick}>点击</button>;
}

这种不一致的行为经常让开发者困惑:为什么同一个函数内,放在 setTimeout 里就变成了同步更新?它可能导致不必要的重绘,也容易造成状态读取的误判。

React 18 的自动批处理:统一异步行为

React 18 引入了自动批处理(Automatic Batching),将批量更新扩展到所有更新来源,包括 setTimeout、原生事件和 Promise。无论在哪里调用 setState,React 都会尽可能将多个更新合并为一个,并在合适的时机异步执行渲染。

这带来了两个显著好处:

  • 性能提升:即使第三方库在异步回调中多次触发状态更新,也会被合并,减少渲染次数。
  • 行为一致性:开发者不再需要区分“合成事件”和“异步回调”的不同更新策略,心智模型更简单。

示例(React 18):

function Demo() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleClick = () => {
    // 即使放在 setTimeout 里,也会自动批处理
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // 只会触发一次重渲染,在本次宏任务结束后同步执行
    }, 0);
  };

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

需要注意的是,自动批处理仍然是异步的count 的值在 setCount 之后不会立即变化,必须通过函数式更新或 useEffect 获取最新值。

需要同步更新的场景:flushSync

在极少数情况下,你可能需要强制 React 同步地应用更新,然后立即读取更新后的 DOM。例如,在状态更新后需要滚动到某个新添加的元素的位置。

React 18 提供了 flushSync 函数,可以强制其内部的 setState 立即执行并同步刷新渲染,打破批处理

import { flushSync } from 'react-dom';

function List() {
  const [items, setItems] = useState([]);

  const addItem = () => {
    // 强制同步更新,添加新项后立即可以操作 DOM
    flushSync(() => {
      setItems(prev => [...prev, { id: Date.now(), text: '新项目' }]);
    });
    // 此时 DOM 已更新,可以滚动到底部
    document.getElementById('list-bottom')?.scrollIntoView();
  };

  return (
    <div>
      <button onClick={addItem}>添加</button>
      <ul>
        {items.map(item => <li key={item.id}>{item.text}</li>)}
      </ul>
      <div id="list-bottom" />
    </div>
  );
}

flushSync 会带来性能损耗,因为它打断了 React 的调度优化,所以仅用于必须同步读取 DOM 的特殊用例,不要滥用。

常见误区与最佳实践

误区 1:以为 setState 是真正意义上的“异步操作”

React 的更新不是 PromisesetTimeout,它只是在内部被延迟批处理了。不要将 setState 与真正的异步 API 等价看待,也不要用 await setState() 的写法(它不返回 Promise)。

误区 2:在更新后立即读取状态

setCount(count + 1);
console.log(count); // 仍然旧值

如果需要基于前一个状态计算新状态,应该使用函数式更新

setCount(prev => prev + 1);

如果需要在状态更新后执行副作用(如请求数据),应该放入 useEffectuseLayoutEffect 中,以状态作为依赖项。

误区 3:担心异步导致性能问题而手动强制同步

多数情况下,React 的批量处理已经是最优策略。强制同步更新反而可能引发渲染卡顿。优先相信 React 的自动批处理,只在需要立即操作 DOM 的特殊场景下考虑 flushSync

总结对比

| 场景 | React 17 及之前 | React 18(默认行为) |
|------|----------------|---------------------|
| 合成事件处理函数内 | 异步批量 | 异步批量 |
| 生命周期函数内 | 异步批量 | 异步批量(类组件仍适用) |
| setTimeout / setInterval 内 | 同步更新 | 异步批量 |
| 原生 DOM 事件 | 同步更新 | 异步批量 |
| Promise / async 回调 | 同步更新 | 异步批量 |
| flushSync 包裹 | 不支持 | 强制同步更新 |

理解同步与异步更新的实质是 React 调度优化的体现。在 React 18 后,你可以默认所有更新都是“异步批量”的,只在极少数需求下使用 flushSync 强行同步。这种统一的心智模型将帮助你编写更可预测、更高效的组件。