3 分钟讲清楚 React 调度原理,快来快来~

193 阅读2分钟

React 内核层

  1. 调度器
    1. Scheduler,核心指责,执行回调。
    2. 把 react-reconciler 提供的回调函数,包装到一个任务对象中。
    3. 内部维护一个任务队列,优先级高的排在最前面(最小堆)。
    4. 循环消费任务队列,直到队列清空。
  1. 构造器
    1. react-reconciler
    2. 接收react-dom和react发起的更新请求。
    3. 将 fiber 树的构造过程包装在一个回调函数中,并将此回调函数传入到 Scheduler 包等待调度。
  1. 渲染器
    1. react-dom 包,2 个核心指责
    2. 引导 react 应用的启动(通过ReactDOM.Render)

概览

image.png

Schedule负责调度 React 生成的任务,reconciler 负责生成 workInprogress 树并交给React-dom渲染到页面上。

常见问题

如果树构造到一半,没有时间片了怎么办。

首先构造到一半,没有时间片之后,是不会把一半的树渲染到页面上的。因为渲染的阶段是 commit,而在执行渲染的过程中会打上标记用来标识是否生成了整颗树。看下源码

 var exitStatus = shouldTimeSlice
        ? renderRootConcurrent(root, lanes)
        : renderRootSync(root, lanes);

      /*KaSong*/ logHook("performConcurrentWorkOnRoot-exitStatus", exitStatus);

      // 可以看到这里,如果exitStatus处于未完成的情况,启动了下一轮的调度,继续构建树。
      // 所以不会出现构建一半的树出现在页面上的情况。因为没走到 commit 阶段
      if (exitStatus !== RootIncomplete) {
        if (exitStatus === RootErrored) {
         
        }

        if (exitStatus === RootFatalErrored) {
          var fatalError = workInProgressRootFatalError;
          prepareFreshStack(root, NoLanes);
          markRootSuspended$1(root, lanes);
          ensureRootIsScheduled(root, now());
          throw fatalError;
        } // Check if this render may have yielded to a concurrent event, and if so,
        // confirm that any newly rendered stores are consistent.
        // TODO: It's possible that even a concurrent render may never have yielded
        // to the main thread, if it was fast enough, or if it expired. We could
        // skip the consistency check in that case, too.

        var renderWasConcurrent = !includesBlockingLane(root, lanes);
        var finishedWork = root.current.alternate;

        if (
          renderWasConcurrent &&
          !isRenderConsistentWithExternalStores(finishedWork)
        ) {
          // A store was mutated in an interleaved event. Render again,
          // synchronously, to block further mutations.
          exitStatus = renderRootSync(root, lanes); // We need to check again if something threw

          /*KaSong*/ logHook("performSyncWorkOnRoot-exitStatus", exitStatus);

          if (exitStatus === RootErrored) {
            var _errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);

            if (_errorRetryLanes !== NoLanes) {
              lanes = _errorRetryLanes;
              exitStatus = recoverFromConcurrentError(root, _errorRetryLanes); // We assume the tree is now consistent because we didn't yield to any
              // concurrent events.
            }
          }

          if (exitStatus === RootFatalErrored) {
            var _fatalError = workInProgressRootFatalError;
            prepareFreshStack(root, NoLanes);
            markRootSuspended$1(root, lanes);
            ensureRootIsScheduled(root, now());
            throw _fatalError;
          }
        } // We now have a consistent tree. The next step is either to commit it,
        // or, if something suspended, wait to commit it after a timeout.

        root.finishedWork = finishedWork;
        root.finishedLanes = lanes;
        finishConcurrentRender(root, exitStatus, lanes);
      }

      ensureRootIsScheduled(root, now());

可以看到用exitStatus进行了判断,满足要求才会进入到 if 语句里面,进而调用 commit,将树交给 ReactDom,最终渲染到页面上。如果是未完全构建的树,是不会进入 commit 阶段的。

触发更新的时机

拿 useState 举例,一个setState就是一个构造 Fiber 树的任务。在 setState内部启动了调度器用来调度任务。

function dispatchSetState(fiber, queue, action) {
  // 开启调度。
        var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
}

开启了一次调度,然后schedule会根据此次任务的 lane 判断当前是同步更新还是并发更新

 // 属于同步渲染,该模式下不可中断。
      if (newCallbackPriority === SyncLane) {
        scheduleMicrotask(flushSyncCallbacks);
      }else{
        // 并发更新
      }

同步更新的话,是一个微任务,并且不可打断。并发更新的话,会根据优先级的不同,在宏任务中触发。并发更新的整个调度过程具有可恢复,可中断,可抢占的特性。