likes
comments
collection
share

setState同步还是异步?

作者站长头像
站长
· 阅读数 22
本文基于React17.0.0

setState的执行流程

简单来说,setState函数是React.Component类的一个方法,它记录了当前应用发生的更新update,并把这些update用一个队列updateQueue记录下来,然后进入scheduleUpdateOnFiber这个函数中进行调度更新,然后触发performSyncWorkOnRoot函数来进入render阶段。最后在commit阶段把更新渲染到dom上。

同步还是异步

为了更加具体的理解setState的执行行为,我们先来看下面的代码,预设我们是在ReactDOM.renderlegacy模式下运行,你可以正确说出两次console.log的结果吗?

constructor(props) {
    super(props);
    this.state = {
      data: "hello"
    }
  }
componentDidMount() {
    //第一个setState
    this.setState({
      data: 'world'
    })
    
    console.log("in componentDidMount: ", this.state.data);

    setTimeout(() => {
      // 第二个setState
      this.setState({
        data: 'Bob'
      })

      console.log("in setTimeout: ", this.state.data);
    })
  }

揭晓答案,结果是 setState同步还是异步?我们可以看到,第一个log输出的是hello,也就是说,当执行第一个setState时,是表现为同步行为的,在执行第二个setState时,log输出的是Bob,是表现为异步行为的。

那么这是为什么呢,可以粗略看出,两个setState执行的环境是不同的,第一个setState的执行环境是在componentDidMount()这个生命周期钩子函数中,第二个setState是在setTimeout中执行的。相信大家一眼就看出来了,问题的本质就是执行环境的不同,导致了setState在执行时的上下文就不一样了,而执行上下文executionContext,就是决定setState同步异步行为的关键。

为何executionContext为何在这个过程中这么重要呢,我们在前面了解到setState在执行过程中会进入 scheduleUpdateOnFiber这个函数,我们先来简单看下在React源码中,有什么蹊跷吧。

scheduleUpdateOnFiber

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  // 省略 .....
  
  if (lane === SyncLane) {// ReactDOM.render 同步执行
    if ( // Check if we're inside unbatchedUpdates
    (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
    (executionContext & (RenderContext | CommitContext)) === NoContext) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, lane); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);

      if (executionContext === NoContext) {
        resetRenderTimer();
        flushSyncCallbackQueue();
      }
    }
  } else {
    // 省略 .....
   } 

    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  } 
  mostRecentlyUpdatedRoot = root;
} 

我们可以看到其中的关键代码

if (executionContext === NoContext) {
        resetRenderTimer();
        flushSyncCallbackQueue();
}

其中flushSyncCallbackQueue函数的作用就是同步执行更新队列里的更新update,前面我们说过了setState后会把更新对象update以队列形式保存下来,这里就是要执行这些更新并清空队列。我们可以看出,如果当前的执行上下文executionContextNoContext,也就是说明React已经不处于自己的调度环节了,而是处于一种无事可做的状态时,React就会去同步的执行setState的回调函数进行更新。那么,什么时候才会是一种无事可做的状态呢?

答案就是不处于React本身的调度阶段时,比如setTimeout,网络请求,直接在Dom节点上绑定的事件等,这些行为都不会触发React的调度行为。当React处于自己的调度阶段时,会根据所处的状态不同给executionContext赋值不同的值,如赋值为BatchedContext时说明进入了更新合并阶段,而executionContext默认情况下就是NoContext。所以不在调度阶段时,React就会进入无事可做的状态,就会将setState同步执行。在React处于自己的调度阶段,会执行诸如生命周期钩子函数,合成事件等,在这些情况下,会触发batchedUpdate进行合并更新,所以此时将executionContext赋值为BatchedContext,那么自然就是异步的行为了。

在我们的例子中,componentDidMount属于React调度流程的一部分,所以其中的setState会被异步执行,componentDidMount执行完后,React就退出调度过程了,此时的executionContextNoContext 了,但是我们的代码还没有执行完,因为setTimeout是一个异步方法,等它的回调被执行的时候,React就会以同步的方式执行setState了。

setState与useState

useState是React为函数组件赋予状态的一种做法,但是它和setState的调用栈有重合的地方,也就是都会进入scheduleUpdateOnFiber函数,所以在同步异步的行为上是没有区别的。