乐趣区

关于前端:setState同步还是异步

本文基于 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);
    })
  }

揭晓答案,后果是

咱们能够看到,第一个 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 函数,所以在同步异步的行为上是没有区别的。

退出移动版