场景

setState 到底是异步的还是同步的?什么情况下是异步,什么情况下是同步?

案例

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            val: 0
        }
    }

    componentDidMount() {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)
    }

    render() {
        return (
            <div>{this.state.val}</div>
        )
    }
}
一个很经典的 setState 问题,两次输出都是 0
看过这章节的 ReactUpdateQueue , ReactDefaultBatchingStrategy , ReactUpdates 应该就能明白为什么会是这样?

分析

ReactUpdateQueue 中,发现 setState 调用了 ReactUpdates 中的 enqueueUpdate :
function enqueueUpdate(component) {
  ensureInjected(); // 保证 batchingStrategy 被注入进来了

  // 如果正在更新, 组件就会被放进 dirtyComponent 中
  if (!batchingStrategy.isBatchingUpdates) {
    // 调用自己, 再次进来的时候, isBatchingUpdates 为 true
    // 相当于把自己放进事务执行
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 会被 flushBatchedUpdates 消费, 批量更新, 只 render 一次
  dirtyComponents.push(component);
}
我们打印堆栈:
对比代码,setState 开始的时候, isBatchingUpdates 已经为 true 了,所以组件直接被放入了 dirtyComponent 中, 并没有执行 batchedUpdates , 所以也并没有执行 flushBatchedUpdates , 也就没有去执行 dirtyComponent 中的 _pendingCallbacks ,所以我们拿到的 state 仍然是以前的。
为什么在 setState 之前,isBatchingUpdates 已经是 true 了?因为在 ReactMount._renderNewRootComponent 中已经调用过一次 ReactUpdates.batchedUpdates , 把组件渲染到 DOM 的过程本身就处于一个 transaction 中。
再看一个例子:
componentDidMount() {
    setTimeout(() => {
      this.setState({
        val: this.state.val + 1
      })
      console.log(this.state.val);
      this.setState({
        val: this.state.val + 1
      })
      console.log(this.state.val);
    }, 0)
}
若是我们改成这样,打印的又是什么?答案是 23 。 问题来了,为什么这次又是同步的了?
很简单,因为 setTimeout 会在下一个事件循环中执行,此时 react 的生命周期都走完了,ReactDefaultBatchingStrategy 事务中的 close 方法将 isBatchingUpdates 设为 false ,所以,会执行 flushBatchedUpdates , 然后去执行 componentperformUpdateIfNecessary , 具体可以看这 performUpdateIfNecessary , 最终执行到 updateComponent ,这个函数会产出最后的 state 并且赋值给组件实例的 state 上,所以这个过程是同步的。
看这个例子:
通过 log,我们发现:

总结

setState 是同步的还是异步的取决于之前有没有执行过 batchedUpdates ,如果有就是异步的。如果没有执行过,就是同步的。