React中setState是同步的还是异步的

50 阅读2分钟

考虑如下代码:

handleClick = () => {
  this.setState({ count: this.state.count + 1 });
  console.log(this.state.count);
};

打印的countsetState后的还是setState前的?

答案是setState前的

这里的setState是异步的

但是如下代码呢?

setTimeout(() => {
  this.setState({ count: this.state.count + 1 });
  console.log(this.state.count);
});

这里打印的是setState后的

这里的setState又变成同步的了

为什么呢?

其实setState在不同场景下的行为是不同的,有时候是异步的,有时候是同步的

异步场景

合成事件

考虑如下代码:

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新前的val --> 0
  }
  render() {
    return (
      <div onClick={this.increment}>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

所有合成事件(如onClick)里面的setState是异步的

生命周期函数

考虑如下代码:

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 输出的还是更新前的值 --> 0
 }
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

所有生命周期函数(如componentDidMount)里面的setState是异步的

同步场景

原生事件

考虑如下代码:

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新后的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

所有原生事件(如addEventListener)里面的setState是同步的

setTimeout、setInternal等异步调用

考虑如下代码:

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 输出更新后的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

所有异步调用(如setTimeout)里面的setState是同步的

原理

那为什么合成事件里是异步的,原生事件里就是同步的呢?它内部是怎么实现的呢?

批量更新

考虑如下代码:

class App extends Component {

  state = { val: 0 }

  batchUpdates = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
 }

  render() {
    return (
      <div onClick={this.batchUpdates}>
        {`Counter is ${this.state.val}`} // 1
      </div>
    )
  }
}

val最终是多少呢?是3吗?

其实是1

为什么呢?

这就不得不说React的批量更新机制了

React会在一次tick中把所有的setState收集到一起,如果是相同的key会被覆盖,取最后一次调用的值,上面的代码会取第三次setState的值,所以就是1

然而这和同步异步有什么关系呢?

异步原理

React把所有的setState收集到一起以后,并不会直接批量更新,而是等当前tick执行完以后,再批量更新

比如,还是上面代码:

class App extends Component {

  state = { val: 0 }

  batchUpdates = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)
 }

  render() {
    return (
      <div onClick={this.batchUpdates}>
        {`Counter is ${this.state.val}`} // 1
      </div>
    )
  }
}
  1. batchUpdates开始执行
  2. 执行到setState,维护一个queue,将三个setState顺序加入queue里面
  3. 执行console.log(this.state.val),打印0
  4. batchUpdates执行完毕
  5. 依次执行queue中的setState,遇到相同的key覆盖前一个

这就是异步的原理

然而setState内部真的是异步执行(比如setTimeout)的吗?

并不是

回到上面的示例,你可能以为第2步执行到setState时,会将setState放入setTimeout里,所以才会在第5步才执行setState

然而React并没有用到setTimeout,上面的五步是同步执行的

所以setState看起来是异步的,但是内部却是同步执行的

同步原理

现在我们知道setState异步原理是React将所有setState收集到一起以后最后批量更新

那为什么在遇到原生事件、异步调用(setTimeout)以后就变成同步的了?难道这些场景下就不批量更新了?

对喽

这些场景不在 React 的“受控上下文”中,所以就不批量更新了

比如:

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}
  1. batchUpdates开始执行
  2. 顺序执行setState
  3. 执行console.log(this.state.val),打印3
  4. batchUpdates执行完毕

可见不批量更新以后,不但setState是同步的,而且不会合并setState,会顺序执行3个setState,所以最终console.log(this.state.val)结果是3

React 18

React 18 引入了自动批量更新,在异步场景中(如 Promise.then、fetch.then、setTimeout),也会尝试合并更新

useState

useStatesetXxx 行为与 class 组件的 setState 一致

总结

我们要区分不同场景下的setState行为,才能更好的避免一些坑