React源码解析之renderRoot概览
- 2019 年 10 月 30 日
- 筆記
在哪里用到 在 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) 前面的三个if
和flushPassiveEffects()
不用去看
(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