面试题:React 中 setState 什么时候是同步的,什么时候是异步的?

这是一道React面试中出现频率极高容易答错的经典题。关键在于理解:setState的“同步/异步”不是由代码本身决定的,而是由调用环境决定的。

下面给出完整、准确的答案。


一、核心结论

在React能够管控的地方,setState是“异步”的;在React管控之外的地方,setState是同步的。

更精确地说:

  • 异步:批量更新(batch update),不会立即更新this.state,多个setState会合并。
  • 同步:立即更新this.state,立即触发重新渲染。

二、异步场景(React管控内)

1. React生命周期方法中

componentDidMount() {
  console.log(this.state.count); // 0
  this.setState({ count: this.state.count + 1 });
  console.log(this.state.count); // 仍然是 0(异步)
}

2. React事件处理函数中(合成事件)

handleClick = () => {
  console.log(this.state.count); // 0
  this.setState({ count: this.state.count + 1 });
  console.log(this.state.count); // 仍然是 0(异步)
}

3. React提供的回调中(如Suspense、startTransition)


三、同步场景(React管控外)

1. 原生DOM事件中

componentDidMount() {
  document.getElementById('btn').addEventListener('click', () => {
    console.log(this.state.count); // 0
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 变为 1(同步!)
  });
}

2. 异步代码中(setTimeout、setInterval、Promise)

handleClick = () => {
  setTimeout(() => {
    console.log(this.state.count); // 0
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 变为 1(同步!)
  }, 0);
}

3. 在原生Promise的then回调中

handleClick = () => {
  Promise.resolve().then(() => {
    console.log(this.state.count); // 0
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 变为 1(同步!)
  });
}

4. 在自定义的DOM事件中

const event = new Event('myEvent');
window.addEventListener('myEvent', () => {
  this.setState({ count: this.state.count + 1 });
});
window.dispatchEvent(event); // 这里的setState是同步的

四、为什么会有这种区别?

React 17 及之前的机制(旧版):

  • React在合成事件生命周期中维护一个批量更新队列(isBatchingUpdates标志)。
  • 进入事件处理函数前,React打开批量更新开关。
  • 所有setState被收集起来,函数执行完后统一更新。
  • 异步代码(setTimeout等)执行时,批量更新开关已经关闭,所以setState立即执行。

React 18 的变化(重要!):

React 18 引入了自动批处理(Automatic Batching)扩大了异步场景

// React 18 中,下面的代码也变成异步了!
setTimeout(() => {
  setCount(c => c + 1);  // 异步(批量)
  setFlag(f => !f);      // 异步(批量)
}, 1000);

// Promise 回调中也异步
fetch('/api').then(() => {
  setCount(c => c + 1);  // 异步(批量)
});

React 18 规则:

  • 只要在React可控的上下文中(包括Promise、setTimeout、原生事件回调等),默认都是异步批量更新。
  • 只有强行脱离React管控(如flushSync)才能变成同步。

五、如何强制同步更新?

使用flushSync(React DOM 提供的API):

import { flushSync } from 'react-dom';

handleClick = () => {
  flushSync(() => {
    this.setState({ count: this.state.count + 1 });
  });
  // 此时 this.state.count 已经更新
  console.log(this.state.count); // 同步
}

六、函数式组件中的情况

在函数组件中,useState返回的setter具有相同的行为

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

const handleClick = () => {
  setCount(1);
  console.log(count); // 仍然是旧值(异步)

  setTimeout(() => {
    setCount(2);
    console.log(count); // React 17 中是旧值? React 18 中是旧值(异步批量)
  }, 100);
};

区别:

  • React 17:setTimeout中是同步的。
  • React 18:setTimeout中也变成异步批量了。

七、面试陷阱与加分点

陷阱1:setState的第二个回调函数

this.setState({ count: 1 }, () => {
  console.log(this.state.count); // ✅ 保证能拿到最新值
});

即使setState是异步的,回调函数会在更新完成后执行。

陷阱2:多个setState的合并

this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 结果只加1(因为对象合并)
// 正确做法:使用函数式更新
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
// 结果加2

加分点(体现深度):

“其实与其说setState是异步的,不如说它是批量延迟更新。React通过维护一个更新队列,在合适的时机(如事件循环结束前)统一执行渲染,从而避免不必要的重复渲染。React 18引入的自动批处理进一步扩大了批处理的覆盖范围,让性能优化更透明。”


八、面试参考答案(精简版)

setState默认是异步批量更新的,在React生命周期和合成事件中不会立即更新this.state,而是合并后统一渲染。但在原生事件、setTimeout、Promise等React管控外的环境中,React 17及之前是同步的。React 18通过自动批处理,将大部分异步环境也变成了批量更新,除非使用flushSync强制同步。获取更新后的值可以通过回调函数或在useEffect中依赖监听。”


快速记忆表

环境React 16/17React 18+
生命周期方法异步异步
合成事件异步异步
setTimeout/Promise同步异步(批量)
原生DOM事件同步异步(批量)
flushSync包裹同步同步
THE END
喜欢就支持一下吧
点赞11 分享