面试官:setState是同步还是异步?

2,644 阅读5分钟

面试官:“React 中 setState 是同步的还是异步?”
我:“异步”
面试官:“有没有可能存在同步的情况?”
我:“……”

setState 是什么?

在 React 中,setState 是类组件中用于更新组件状态的核心 API。当你调用 setState 时,它会请求 React 重新渲染组件,并更新 UI。

参数

setState(nextState, callback?)

  • nextState:一个对象或者函数。

    • 如果你传递一个对象作为 nextState,它将浅层合并到 this.state 中。
    • 如果你传递一个函数作为 nextState,它将被视为 更新函数。它必须是个纯函数,应该以已加载的 state 和 props 作为参数,并且应该返回要浅层合并到 this.state 中的对象。React 会将你的更新函数放入队列中并重新渲染你的组件。在下一次渲染期间,React 将通过应用队列中的所有的更新函数来计算下一个 state。
  • 可选的 callback:如果你指定该函数,React 将在提交更新后调用你提供的 callback

想必各位大佬都用地很熟练了,就不过多赘述。

批处理更新机制

有时候,大家可能会遇到一个常见问题:多次调用 setState 并不会依次生效,最后只会应用最后一次的 setState 结果。原因在于 React 的异步批量更新机制。

class App extends React.Component {
  state = {
    count: 1000,
  };

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
  };
  render() {
    return (
      <div className="App">
        <button onClick={this.handleClick}>+1</button>
        <p>count:{this.state.count}</p>
      </div>
    );
  }
}
GIF 2024-10-9 15-46-57.gif

上述代码不会导致 count 增加两次,而是仅更新一次。这是因为 setState 的调用是批量处理的,React 合并了两次更新,最终只执行了一次。

每次 setState 调用会触发一次重新渲染,因此它是一个影响性能的操作。为了优化渲染过程,React 引入了异步的批处理更新机制,确保多次状态更新可以合并为一次渲染。

React 的批量更新机制通过一种叫做 “事务机制(transactional batching)” 的方式进行管理。当你调用 setState 时,React 会将这些状态更新操作放入一个队列,等事件处理结束后再进行一次批量更新。这就避免了每次 setState 调用都触发重渲染的问题。

setState 到底是同步还是异步?

这个问题得分情况:如果在 React18 之前你可以说 既是异步又是同步;但是在 React18 ,setState 被归为异步操作。

React 18 之前

在 React 18 之前,我们常说合成事件处理函数生命周期方法通常是异步的,因为在这个版本中 setState 会在这些场景中进行批量化处理,将这些状态更新操作放入一个队列,等事件处理结束后再进行一次批量更新,这就导致为什么是异步的。

而当你在 React 18 之前的原生 DOM 事件或者setTimeout 中调用 setState 时,setState同步的。React 不会进行批量更新,因此状态会立即更新。

这就是为什么React 18 之前我们说setState既是异步又是同步。

React 18 之后

一句话来说就是 —— 任何情况都会自动执行批处理,多次更新始终合并为一次

举个例子🌰:

// React 17
componentDidMount() {
  document.getElementById('btn').addEventListener('click', () => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);  // 1001
  });
}

// React 18
componentDidMount() {
  document.getElementById('btn').addEventListener('click', () => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);  // 1000
  });
}
  • 在 React 17 中,setState 会在原生事件处理程序中同步更新,因此你会立即看到更新后的状态值。
  • 在 React 18 中,即使在原生事件处理程序中setState 也会被批量处理,因此状态更新不再是同步的,而是异步的。

setState 同步做法

在 React18 之后如果想实现同步的效果,即调 setState 后立马拿到最新数据,官方很贴心考虑到这一点,在 React18 新提供了一个API —— flushSync

官方原话是这样讲的:

setState 视为 请求 而会不是立即更新组件的命令。当多个组件更新它们的 state 来响应事件时,React 将批量更新它们,并在这次事件结束时将它们一并重新渲染。在极少数情况下,你需要强制同步应用特定的 state 更新,这时你可以将其包装在 flushSync 中,但这可能会损害性能。

用例:

import { flushSync } from 'react-dom'

// 这里相当于一次批处理,flushSync回调执行完后立马执行一次render函数
flushSync(() => {
  this.setState({
    text: 'Hello,React 18'
  })
});

console.log(this.state.text); // 'Hello,React 18'

总结

React 18 后 setState 是异步的

在 React 18 后,setState 被设计为在所有上下文中都是异步的,包括合成事件、原生事件、PromisesetTimeout 等。这是 React 的一种优化机制,用于支持更复杂的并发渲染模式(Concurrent Mode)和提升性能。

所以,如果你真的面试时被问到这个问题,你就说:

  • 在 React 18 之前,setState 的行为可能会根据场景表现为同步或异步。
  • 然而在 React 18 中,setState 统一变成了异步的,通过批处理优化渲染性能。

参考

最后

码字不易,感谢三连!

已将学习代码上传至 github,欢迎大家学习指正!

技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 “点赞 收藏+关注” ,感谢支持!!