关于react 的 setState 同步还是异步

146 阅读4分钟

在React中,setState()的更新可以是异步的也可以是同步的,这取决于它被调用的上下文

同步还是异步?

  • 异步更新: 当在React的生命周期方法或由React控制的事件处理函数中调用setState()时,React将批处理多个setState()调用以提高性能。因此,在这些情况下,setState()表现得像是异步的(尽管它并不真正创建新的线程或执行任何异步操作)。
  • 同步更新: 但是,如果setState()在setTimeout()、原生事件处理函数、或异步代码中被调用,那么它更新状态的行为就变成了同步的。这是因为在这些场景下,React已经没有机会去批处理状态更新了。

不同版本的区别

在React 16和之前的版本中,setState()主要在合成事件、生命周期函数中是“异步”的,在setTimeout等原生事件处理函数中是“同步”的。 在React中,有所谓的Concurrent Mode(并发模式),它在React 18中正式作为可选功能被引入。在并发模式中,setState()调用更多是基于优先级的而不是简单的同步或异步。并发模式允许React根据任务的重要性和紧急程度抉择它们何时被处理。因此,在并发模式中,setState()的表现可能会更加复杂和不同于之前版本的React。

举例说明

考虑以下常见的异步更新setState()示例:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });

    // 输出的count值不会立即反映上面已被调用了三次的setState
    console.log(this.state.count); // 结果可能是0
  }

  render() {
    // 当React最后处理完状态更新后,count将变为1而非3,因为上述调用被批处理,并且多个相同key的更新被合并
    return <div>{this.state.count}</div>;
  }
}

而对于同步更新的setState(),请看以下示例:

class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { // 非React事件中使用setState将同步更新状态 setTimeout(() => { this.setState({ count: this.state.count + 1 }); console.log(this.state.count); // 现在会立即反映更新,输出1 this.setState({ count: this.state.count + 1 }); console.log(this.state.count); // 再次反映更新,输出2 }, 0); } render() { return <div>{this.state.count}</div>; } }

在React异步更新状态可能会导致脱节,这通常可以通过使用回调函数或者setState()的函数形式来解决,比如:

this.setState((prevState, props) => ({
  count: prevState.count + 1
}));
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    // 使用函数形式的setState,确保每次状态的更新都是基于最新的状态值
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });

    // 由于是异步更新,下面的日志会在实际更新之前被调用,输出可能还是0
    console.log(this.state.count); // 可能输出0
  }

  render() {
    // 当React最后处理完状态更新后,count将变为3,因为使用了prevState确保每次都是增加1
    return <div>{this.state.count}</div>;
  }
}

这个例子也遵循React的异步更新模型:在生命周期方法和合成事件处理器中,状态更新是批量处理的。在React异步执行状态更新时,它会把所有的更新加入一个队列,然后经过优化一起执行以达到最佳性能。通过这种方式,React 并不会为每次调用setState()执行一个单独的更新,而是会对更新进行批次处理,从而减少不必要的渲染和计算量。 这样异步的状态更新行为是React旨在优化性能和避免潜在的问题(如渲染抖动)的结果。然而,它也可能造成开发者混淆,因为代码的执行看起来完全是同步的,但实际上可能会有所延迟。开发者需要熟悉这种模型才能正确地理解和预测他们的应用程序的行为。 React 18引入的并发模式(Concurrent Mode)进一步增加了更新执行的灵活性,允许React "中断"渲染过程以确保有更高优先级的更新可以首先执行。这意味着,React可以在处理大量更新时开始渲染,然后如果有任何急需处理的更新(如用户输入),它可以将这些更新移到队列前面。这不仅优化了性能感知,也提供了更ヌ㒠的用户界面交互反馈。 了解并发模式和setState()在不同上下文中的行为对于构建响应迅速且性能出色的React应用很关键。随着技术的不断进步,React的更新机制可能会继续变化,开发者需要跟进这些变化以充分利用React提供的新特性和改进。

从源码上找根本原因