React源码解析之renderRoot概览

  • 2019 年 10 月 30 日
  • 筆記

前言: 由于个人能力和精力有限,外加 renderRoot 的内容太多,里面的每个 function 都可以单独拿出来讲,所以本篇文章的目的是帮助大家了解 renderRoot 大概做了哪些事,而这些事具体的内容,以后会一个个单独拿出来解析。

在哪里用到React源码解析之scheduleWork(上)中:

export function scheduleUpdateOnFiber(){    xxx    xxx    xxx    if (expirationTime === Sync) {      if( 第一次render ){        xxx        //初始化root,调用workLoop进行循环单元更新        let callback = renderRoot(root, Sync, true);      }else{        xxx      }    }  }  

一、renderRoot 主要的作用: (1) 调用 workLoop 进行循环单元更新 (2) 捕获错误并进行处理 (3) 走完流程后,根据workInProgressRoot的不同状态来进行不同的操作

源码:

function renderRoot(    root: FiberRoot,    expirationTime: ExpirationTime,    isSync: boolean,  ): SchedulerCallback | null {    invariant(      (executionContext & (RenderContext | CommitContext)) === NoContext,      'Should not already be working.',    );      if (enableUserTimingAPI && expirationTime !== Sync) {      const didExpire = isSync;      stopRequestCallbackTimer(didExpire);    }      if (root.firstPendingTime < expirationTime) {      // If there's no work left at this expiration time, exit immediately. This      // happens when multiple callbacks are scheduled for a single root, but an      // earlier callback flushes the work of a later one.      return null;    }      if (isSync && root.finishedExpirationTime === expirationTime) {      // There's already a pending commit at this expiration time.      // TODO: This is poorly factored. This case only exists for the      // batch.commit() API.      return commitRoot.bind(null, root);    }    // 听说是useEffect的调用    flushPassiveEffects();      // If the root or expiration time have changed, throw out the existing stack    // and prepare a fresh one. Otherwise we'll continue where we left off.      /*nextRoot =》 workInProgressRoot*/    /*nextRenderExpirationTime =》 renderExpirationTime*/    //workInProgressRoot 指接下来要更新的节点    //renderExpirationTime 指接下来更新节点的过期时间    //意思就是当前要更新的节点并非是队列中要更新的节点,也就是说被新的高优先级的任务给打断了    if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {      //重置调度队列,并从root节点(新的高优先级的节点)开始调度      /*resetStack <=> prepareFreshStack */      prepareFreshStack(root, expirationTime);      //将调度优先级高的interaction加入到interactions中      startWorkOnPendingInteractions(root, expirationTime);    }    //应该是当已经接收一个低优先级的要更新的节点时所进行的操作    else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {      // We could've received an update at a lower priority while we yielded.      // We're suspended in a delayed state. Once we complete this render we're      // just going to try to recover at the last pending time anyway so we might      // as well start doing that eagerly.      // Ideally we should be able to do this even for retries but we don't yet      // know if we're going to process an update which wants to commit earlier,      // and this path happens very early so it would happen too often. Instead,      // for that case, we'll wait until we complete.      if (workInProgressRootHasPendingPing) {        // We have a ping at this expiration. Let's restart to see if we get unblocked.        prepareFreshStack(root, expirationTime);      } else {        const lastPendingTime = root.lastPendingTime;        if (lastPendingTime < expirationTime) {          // There's lower priority work. It might be unsuspended. Try rendering          // at that level immediately, while preserving the position in the queue.          return renderRoot.bind(null, root, lastPendingTime);        }      }    }      // If we have a work-in-progress fiber, it means there's still work to do    // in this root.    if (workInProgress !== null) {      const prevExecutionContext = executionContext;      executionContext |= RenderContext;      let prevDispatcher = ReactCurrentDispatcher.current;      if (prevDispatcher === null) {        // The React isomorphic package does not include a default dispatcher.        // Instead the first renderer will lazily attach one, in order to give        // nicer error messages.        prevDispatcher = ContextOnlyDispatcher;      }      ReactCurrentDispatcher.current = ContextOnlyDispatcher;      let prevInteractions: Set<Interaction> | null = null;      if (enableSchedulerTracing) {        prevInteractions = __interactionsRef.current;        __interactionsRef.current = root.memoizedInteractions;      }      //绑定 currentFiber,也标志着开始执行 workloop      startWorkLoopTimer(workInProgress);        // TODO: Fork renderRoot into renderRootSync and renderRootAsync      //如果是同步的话      if (isSync) {        //如果更新时间是异步的话        if (expirationTime !== Sync) {          // An async update expired. There may be other expired updates on          // this root. We should render all the expired work in a          // single batch.            //将所有过期的时间分批次处理          const currentTime = requestCurrentTime();          if (currentTime < expirationTime) {            // Restart at the current time.            executionContext = prevExecutionContext;            resetContextDependencies();            ReactCurrentDispatcher.current = prevDispatcher;            if (enableSchedulerTracing) {              __interactionsRef.current = ((prevInteractions: any): Set<                Interaction,              >);            }            return renderRoot.bind(null, root, currentTime);          }        }      } else {        // Since we know we're in a React event, we can clear the current        // event time. The next update will compute a new event time.          //清除currentEventTime        currentEventTime = NoWork;      }        do {        try {          //执行每个节点的更新          if (isSync) {            workLoopSync();          } else {            //判断是否需要继续调用performUnitOfWork            workLoop();          }            break;        }        //捕获异常,并处理        catch (thrownValue)        {          // Reset module-level state that was set during the render phase.          //重置状态          resetContextDependencies();          resetHooks();            const sourceFiber = workInProgress;          /*nextUnitOfWork <=> sourceFiber*/          //如果sourceFiber是存在的,那么 React 可以判断错误的原因          //如果sourceFiber是不存在的,说明是未知错误          if (sourceFiber === null || sourceFiber.return === null) {            // Expected to be working on a non-root fiber. This is a fatal error            // because there's no ancestor that can handle it; the root is            // supposed to capture all errors that weren't caught by an error            // boundary.            //重置调度队列,并从root节点(新的高优先级的节点)开始调度            prepareFreshStack(root, expirationTime);            executionContext = prevExecutionContext;            //抛出错误            throw thrownValue;          }          //记录error被捕获前,渲染所花费的时间          //这样可以避免在渲染挂起(暂停)的情况下,Profiler的时间会不准确            //Profiler:测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。          //它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。          if (enableProfilerTimer && sourceFiber.mode & ProfileMode) {            // Record the time spent rendering before an error was thrown. This            // avoids inaccurate Profiler durations in the case of a            // suspended render.            stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true);          }          //获取父节点          const returnFiber = sourceFiber.return;          //抛出可预期的错误          throwException(            root,            returnFiber,            sourceFiber,            thrownValue,            renderExpirationTime,          );          //完成对sourceFiber的渲染,          //但是因为已经是报错的,所以不会再渲染sourceFiber的子节点了          workInProgress = completeUnitOfWork(sourceFiber);        }      } while (true);        executionContext = prevExecutionContext;      //重置状态      resetContextDependencies();      ReactCurrentDispatcher.current = prevDispatcher;      if (enableSchedulerTracing) {        __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);      }      //如果仍有正在进程里的任务      if (workInProgress !== null) {        // There's still work left over. Return a continuation.        //停止计时        stopInterruptedWorkLoopTimer();        if (expirationTime !== Sync) {          //开始调度callback的标志          startRequestCallbackTimer();        }        //绑定 this        return renderRoot.bind(null, root, expirationTime);      }    }      // 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.    // 至此,保证了 fiber 树的每个节点的状态都是一致的。接下来会执行 commit 步骤/或者是又有新的任务被挂起了,等待挂起结束再去 commit    stopFinishedWorkLoopTimer();      root.finishedWork = root.current.alternate;    root.finishedExpirationTime = expirationTime;    //判断当前节点是否被阻止commit    const isLocked = resolveLocksOnRoot(root, expirationTime);    //如果有,则退出    if (isLocked) {      // This root has a lock that prevents it from committing. Exit. If we begin      // work on the root again, without any intervening updates, it will finish      // without doing additional work.      return null;    }      // Set this to null to indicate there's no in-progress render.    //将workInProgressRoot以告诉 react 没有正在 render 的进程    workInProgressRoot = null;    //根据workInProgressRoot的不同状态来进行不同的操作    switch (workInProgressRootExitStatus) {      case RootIncomplete: {        invariant(false, 'Should have a work-in-progress.');      }      // Flow knows about invariant, so it compains if I add a break statement,      // but eslint doesn't know about invariant, so it complains if I do.      //对下面 eslint 注释的解释,可不看      // eslint-disable-next-line no-fallthrough      case RootErrored: {        // An error was thrown. First check if there is lower priority work        // scheduled on this root.        const lastPendingTime = root.lastPendingTime;        if (lastPendingTime < expirationTime) {          // There's lower priority work. Before raising the error, try rendering          // at the lower priority to see if it fixes it. Use a continuation to          // maintain the existing priority and position in the queue.          return renderRoot.bind(null, root, lastPendingTime);        }        if (!isSync) {          // If we're rendering asynchronously, it's possible the error was          // caused by tearing due to a mutation during an event. Try rendering          // one more time without yiedling to events.          prepareFreshStack(root, expirationTime);          scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));          return null;        }        // If we're already rendering synchronously, commit the root in its        // errored state.        return commitRoot.bind(null, root);      }      case RootSuspended: {        // We have an acceptable loading state. We need to figure out if we should        // immediately commit it or wait a bit.          // If we have processed new updates during this render, we may now have a        // new loading state ready. We want to ensure that we commit that as soon as        // possible.        const hasNotProcessedNewUpdates =          workInProgressRootLatestProcessedExpirationTime === Sync;        if (hasNotProcessedNewUpdates && !isSync) {          // If we have not processed any new updates during this pass, then this is          // either a retry of an existing fallback state or a hidden tree.          // Hidden trees shouldn't be batched with other work and after that's          // fixed it can only be a retry.          // We're going to throttle committing retries so that we don't show too          // many loading states too quickly.          let msUntilTimeout =            globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();          // Don't bother with a very short suspense time.          if (msUntilTimeout > 10) {            if (workInProgressRootHasPendingPing) {              // This render was pinged but we didn't get to restart earlier so try              // restarting now instead.              prepareFreshStack(root, expirationTime);              return renderRoot.bind(null, root, expirationTime);            }            const lastPendingTime = root.lastPendingTime;            if (lastPendingTime < expirationTime) {              // There's lower priority work. It might be unsuspended. Try rendering              // at that level.              return renderRoot.bind(null, root, lastPendingTime);            }            // The render is suspended, it hasn't timed out, and there's no lower            // priority work to do. Instead of committing the fallback            // immediately, wait for more data to arrive.            root.timeoutHandle = scheduleTimeout(              commitRoot.bind(null, root),              msUntilTimeout,            );            return null;          }        }        // The work expired. Commit immediately.        return commitRoot.bind(null, root);      }      case RootSuspendedWithDelay: {        if (!isSync) {          // We're suspended in a state that should be avoided. We'll try to avoid committing          // it for as long as the timeouts let us.          if (workInProgressRootHasPendingPing) {            // This render was pinged but we didn't get to restart earlier so try            // restarting now instead.            prepareFreshStack(root, expirationTime);            return renderRoot.bind(null, root, expirationTime);          }          const lastPendingTime = root.lastPendingTime;          if (lastPendingTime < expirationTime) {            // There's lower priority work. It might be unsuspended. Try rendering            // at that level immediately.            return renderRoot.bind(null, root, lastPendingTime);          }            let msUntilTimeout;          if (workInProgressRootLatestSuspenseTimeout !== Sync) {            // We have processed a suspense config whose expiration time we can use as            // the timeout.            msUntilTimeout =              expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();          } else if (workInProgressRootLatestProcessedExpirationTime === Sync) {            // This should never normally happen because only new updates cause            // delayed states, so we should have processed something. However,            // this could also happen in an offscreen tree.            msUntilTimeout = 0;          } else {            // If we don't have a suspense config, we're going to use a heuristic to            // determine how long we can suspend.            const eventTimeMs: number = inferTimeFromExpirationTime(              workInProgressRootLatestProcessedExpirationTime,            );            const currentTimeMs = now();            const timeUntilExpirationMs =              expirationTimeToMs(expirationTime) - currentTimeMs;            let timeElapsed = currentTimeMs - eventTimeMs;            if (timeElapsed < 0) {              // We get this wrong some time since we estimate the time.              timeElapsed = 0;            }              msUntilTimeout = jnd(timeElapsed) - timeElapsed;              // Clamp the timeout to the expiration time.            // TODO: Once the event time is exact instead of inferred from expiration time            // we don't need this.            if (timeUntilExpirationMs < msUntilTimeout) {              msUntilTimeout = timeUntilExpirationMs;            }          }            // Don't bother with a very short suspense time.          if (msUntilTimeout > 10) {            // The render is suspended, it hasn't timed out, and there's no lower            // priority work to do. Instead of committing the fallback            // immediately, wait for more data to arrive.            root.timeoutHandle = scheduleTimeout(              commitRoot.bind(null, root),              msUntilTimeout,            );            return null;          }        }        // The work expired. Commit immediately.        return commitRoot.bind(null, root);      }      case RootCompleted: {        // The work completed. Ready to commit.        if (          !isSync &&          workInProgressRootLatestProcessedExpirationTime !== Sync &&          workInProgressRootCanSuspendUsingConfig !== null        ) {          // If we have exceeded the minimum loading delay, which probably          // means we have shown a spinner already, we might have to suspend          // a bit longer to ensure that the spinner is shown for enough time.          const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(            workInProgressRootLatestProcessedExpirationTime,            expirationTime,            workInProgressRootCanSuspendUsingConfig,          );          if (msUntilTimeout > 10) {            root.timeoutHandle = scheduleTimeout(              commitRoot.bind(null, root),              msUntilTimeout,            );            return null;          }        }        return commitRoot.bind(null, root);      }      default: {        invariant(false, 'Unknown root exit status.');      }    }  }  

解析: (1) 前面的三个ifflushPassiveEffects()不用去看

(2) 如果render的时候,有个更高优先级的任务插进来要执行的话,需执行prepareFreshStack,重置调度队列,并从root节点(新的高优先级的节点)开始调度

(3) else if (workInProgressRootExitStatus === RootSuspendedWithDelay)的部分不看,应该是当已经接收一个低优先级的要更新的节点时所进行的操作

(4) 当该节点上面仍有未执行的任务时,执行startWorkLoopTimer(),绑定currentFiber,也标志着开始执行workloop

(5) if (isSync) { xxx } else { xxx }可不看

(6) 调用workLoop()/workLoopSync()进行循环单元的更新,这个方法以后会详细解析

(7) 捕获异常,并处理,重点是throwException方法,用来抛出可预期的错误,以后解析

(8) 根据workInProgressRoot的不同状态来进行不同的操作,也就是当root被处理完,根据它的状态,来判断后续是进行commit、阻止commit并重新render、等待一段时间再去commit、还是报错。

GitHub: https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberWorkLoop.js