React setState 是同步还是异步

208 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

核心描述

  • 说明:所谓同步还是异步,指的是调用 setState 方法之后是否马上能得到新的 state 值。

  • 表现:

    • 常规模式 legacy 模式下:
      • index.js 核心代码
      // 默认渲染为 legacy 模式
      ReactDOM.render(
        <React.StrictMode>
          <App />
        </React.StrictMode>,
        document.getElementById('root')
      );
      
      
      • 示例核心代码:
      // 创建一个类组件,在 componentDidMount 钩子中编写 demo
      componentDidMount () {
          // 表现为同步输出
          setTimeout(()=>{
              this.setState({number:3})
              console.log(this.state.number) // 输出 3
              this.setState({number:4})
              console.log(this.state.number) // 输出 4
          },0)
          // 表现为异步输出
          this.setState({
              a:1
          })
          console.log(this.state.a) // 输出 undefined
      }
      
    • concurrent 模式下:
      • index.js 核心代码:
          import ReactDOM from 'react-dom';
          // 启动 concurrent 渲染模式
          ReactDOM.unstable_createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>);
      
      • 示例核心代码:
          // 表现为异步输出
          setTimeout(()=>{
              this.setState({number:3})
              console.log(this.state.number) // 输出 undefined
              this.setState({number:4})
              console.log(this.state.number) // 输出 undefined
          },0)
          // 表现为异步输出
          this.setState({
              a:1
          })
          console.log(this.state.a) // 输出 undefined
      
  • 结论:在不同的情况下,会表现出不同的结果。如果 this.setState 方法调用进入的 React 内部的任务调度,则会表现为异步;如果未进入 React 的任务调度,则会表现为同步。这是 React 内部合并 setState 执行的一种性能优化的实现。

    • 同步的表现情况:
      • 首先在 legacy 模式下
      • 在执行上下文为空的时候去调用 setState
        • 可以使用异步调用如setTimeout, Promise, MessageChannel等
        • 可以监听原生事件, 注意不是合成事件, 在原生事件的回调函数中执行 setState 就是同步的
    • 异步的表现情况:
      • 如果是合成事件中的回调, executionContext |= DiscreteEventContext, 所以不会进入, 最终表现出异步
      • concurrent 模式下都会为异步

知识拓展

源码参考 React 版本为 v17.0.2

  • 在 React 中调用 setState 方法时,影响其同步/异步表现调度核心方法为 scheduleUpdateOnFiber
  • setState 核心调用链:this.setState -> this.updater.enqueueSetState -> scheduleUpdateOnFiber
  • 源码分析:
checkForNestedUpdates();
  warnAboutRenderPhaseUpdatesInDEV(fiber);
  var root = markUpdateLaneFromFiberToRoot(fiber, lane);

  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return null;
  } // Mark that the root has a pending update.


  markRootUpdated(root, lane, eventTime);

  if (root === workInProgressRoot) {
    // Received an update to a tree that's in the middle of rendering. Mark
    // that there was an interleaved update work on this root. Unless the
    // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
    // phase update. In that case, we don't treat render phase updates as if
    // they were interleaved, for backwards compat reasons.
    {
      workInProgressRootUpdatedLanes = mergeLanes(workInProgressRootUpdatedLanes, lane);
    }

    if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
      // The root already suspended with a delay, which means this render
      // definitely won't finish. Since we have a new update, let's mark it as
      // suspended now, right before marking the incoming update. This has the
      // effect of interrupting the current render and switching to the update.
      // TODO: Make sure this doesn't override pings that happen while we've
      // already started rendering.
      markRootSuspended$1(root, workInProgressRootRenderLanes);
    }
  } // TODO: requestUpdateLanePriority also reads the priority. Pass the
  // priority as an argument to that function and this one.


  var priorityLevel = getCurrentPriorityLevel();

  if (lane === SyncLane) {
    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
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.
      console.log('=====> debuger: 01',lane, SyncLane)
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);
      console.log('======> debugger:02', executionContext, NoContext) // 在 legacy 模式下,会打印此 log,在 setTimeout 方法调用时, executionContext 与 NoContext 相等
      if (executionContext === NoContext) {
        // Flush the synchronous work now, unless we're already working or inside
        // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
        // scheduleCallbackForFiber to preserve the ability to schedule a callback
        // without immediately flushing it. We only do this for user-initiated
        // updates, to preserve historical behavior of legacy mode.
        resetRenderTimer();
        flushSyncCallbackQueue();
      }
    }
  } else {
    // Schedule a discrete update but only if it's not Sync.
    if ((executionContext & DiscreteEventContext) !== NoContext && ( // Only updates at user-blocking priority or greater are considered
    // discrete, even inside a discrete event.
    priorityLevel === UserBlockingPriority$2 || priorityLevel === ImmediatePriority$1)) {
      // This is the result of a discrete event. Track the lowest priority
      // discrete update per root so we can flush them early, if needed.
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Set([root]);
      } else {
        rootsWithPendingDiscreteUpdates.add(root);
      }
    } // Schedule other updates after in case the callback is sync.

    console.log('======> debugger:03') // 当使用 concurrent 模式时,会输出此 log
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }

参考资料

浏览知识共享许可协议

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。