renderRoot
1 )概述
renderRoot
是一个非常复杂的方法- 这个方法里处理很多各种各样的逻辑, 它主要的工作内容是什么?
- A. 它调用
workLoop
进行循环单元更新- 遍历整个 Fiber Tree,把每一个组件或者 dom 节点对应的
- Fiber 节点拿出来单一的进行更新,这是一个循环的操作
- 把整棵 Fiber Tree 都遍历一遍,这就是
workLoop
- B. 捕获错误并进行处理
- 在进行每一个单元更新的时候,这个遍历逻辑的时候,有可能会出现一些错误
- 有些是可预期的,比如说是 Suspense 的功能, throw 一个 Promise 对象
- 这个时候,是我们要特殊的对它进行处理的
- 有一些是不可预期的,比如说, render 里面报了一个错误
- 我们也要对它进行一些处理,要告诉我们的react这个地方
- 我们出现了一个错误,在react16之后,有了
Error Boundary
- 可以在组件内捕获渲染的错误
- C. 在走完流程之后要进行善后
- 因为流程走完之后会有各种不同的情况
- 比如说有错误的情况,比如说有任务被挂起的情况,也就是Suspense的情况
- 这些任务,都要按照特定的逻辑给它进行一些处理
- 这就是
renderRoot
这个方法,它的主要的核心工作内容
2 )流程图
- 进入 renderRoot, 它里面有一个很核心的循环
do while workLoop
- 这个while循环就是调用 workLoop 对整棵树,它每个节点进行一个遍历,并且拿出来单独进行更新
- 因为每个 Fiber节点上,如果有更新的话,它会记入 updateQueen
- 我们通过 updateQueen 上是否有内容来判断它是否要进行更新
- 以及可以计算出它的新的 state,得到最新的 children,拿到所有最新的节点
- 在 workLoop 的过程当中,它在做什么呢?
nextUnitOfWork
就是每一个节点在遍历的过程当中,它自己更新完之后,它会返回它的第一个child- 它的第一个child 就是作为 nextUnitOfWork,因为我们执行了一个节点的更新之后,我们需要返回
- 返回之后,我们要判断一些逻辑
- 比如,对于异步的操作,每个节点更新完之后都要判断
!shouldYield()
- 判断我们现在的时间片是否还有?如果还有的话,再继续,如果没有的话,就要跳出了
- 接下去就会执行
performUnitOfWork
- 之后,执行
beginWork
,completeUnitOfWork
这些,当然中间会判断是否有 next - next就是我在更新完一个结点之后,它是否还有下一个节点需要更新
- 如果有next的情况,我们就返回,然后去判断这个逻辑是否还有
- 这就是整个对整个 fiber tree 每个节点的遍历更新
- 在这个更新过程当中,如果有任何catch,就是捕获到异常
- 那么首先会进行一系列的判断,然后对它执行 throwException 或者是 onUncaughtError
- 它们对应的逻辑会不一样, 如果这个节点它是可处理的错误
- 我会直接对它进行
completeUnitOfWork()
因为更新到这个节点之后,它抛出错误了 - 说明我们这个节点下面的所有子节点都不需要再更新了
- 执行完成之后,我们就会调用 continue,对于这个 do while 循环,它又继续调用 workLoop
- 也就是说我们把一棵子数的错误处理完之后,它还可以继续对别的子树进行更新
- 整体更新完之后,就会 break,之后会有各种不同的情况,比如说有致命的错误等
- 它们都会调用不同的逻辑进行一个处理,这里
nextRenderDidError
是一个可处理的错误 - 比如说, 是可以被捕获的错误,有组件能捕获它,而
nextLatestAbsoluteTimeoutMs
是抛出promise的错误 - 它是一个被挂起的一个任务,对应要执行一个被被挂起的操作, 最后如果上面的情况都没有出现
- 直接
onComplete
,之后就可以在root节点上set finishedwork
,这样, 就可以对它整体进行一个更新 - 就可以执行
completeRoot
,就可以把 fiber树 变成我们真正的dom 树去更新整个页面 - 这就是整个 renderRoot 它的一个逻辑,简单总结
- 先正常的执行每个单元的更新
- 然后捕获到任何错误进行一定的处理
- 最终把整个树遍历完之后根据不同的情况再进行一个处理
3 )源码
定位到 packages/react-reconciler/src/ReactFiberScheduler.js
先看 renderRoot 的代码
function renderRoot(
root: FiberRoot,
isYieldy: boolean,
isExpired: boolean,
): void {
invariant(
!isWorking,
'renderRoot was called recursively. This error is likely caused ' +
'by a bug in React. Please file an issue.',
);
isWorking = true;
ReactCurrentOwner.currentDispatcher = Dispatcher;
const expirationTime = root.nextExpirationTimeToWorkOn;
// Check if we're starting from a fresh stack, or if we're resuming from
// previously yielded work.
// 刚进来的时候,进行这个判断
// nextRoot 和 nextRenderExpirationTime 对应着接下来要渲染的节点和对应的过期时间 这是两个公共变量
// 在这种情况下,说明调用这个方法的时候,接收到的参数和之前的不一样,可能就是之前的异步任务被新进来的高优先级的任务给打断了
if (
expirationTime !== nextRenderExpirationTime ||
root !== nextRoot ||
nextUnitOfWork === null
) {
// Reset the stack and start working from the root.
resetStack();
nextRoot = root;
nextRenderExpirationTime = expirationTime;
// nextUnitOfWork 来自于 createWorkInProgress
// 就是把当前的应用的状态对应的Fiber节点,拷贝了一份叫做 workInProgress 的对象
// 因为我们不能直接在当前对象的Fiber节点上操作,它会影响我们目前的dom节点展示的样子
// 所以要复制一份拷贝,对拷贝进行操作,workInProgress 和 current 之间会有一个转换的关系
// 在renderRoot开始之后,我们真正操作的节点都是 workInProgress,没有直接在 current 上操作
nextUnitOfWork = createWorkInProgress(
nextRoot.current,
null,
nextRenderExpirationTime,
);
// 这种和不同expirationTime会有关系
root.pendingCommitExpirationTime = NoWork;
if (enableSchedulerTracing) {
// Determine which interactions this batch of work currently includes,
// So that we can accurately attribute time spent working on it,
// And so that cascading work triggered during the render phase will be associated with it.
const interactions: Set<Interaction> = new Set();
root.pendingInteractionMap.forEach(
(scheduledInteractions, scheduledExpirationTime) => {
if (scheduledExpirationTime <= expirationTime) {
scheduledInteractions.forEach(interaction =>
interactions.add(interaction),
);
}
},
);
// Store the current set of interactions on the FiberRoot for a few reasons:
// We can re-use it in hot functions like renderRoot() without having to recalculate it.
// We will also use it in commitWork() to pass to any Profiler onRender() hooks.
// This also provides DevTools with a way to access it when the onCommitRoot() hook is called.
root.memoizedInteractions = interactions;
if (interactions.size > 0) {
const subscriber = __subscriberRef.current;
if (subscriber !== null) {
const threadID = computeThreadID(
expirationTime,
root.interactionThreadID,
);
try {
subscriber.onWorkStarted(interactions, threadID);
} catch (error) {
// Work thrown by an interaction tracing subscriber should be rethrown,
// But only once it's safe (to avoid leaveing the scheduler in an invalid state).
// Store the error for now and we'll re-throw in finishRendering().
if (!hasUnhandledError) {
hasUnhandledError = true;
unhandledError = error;
}
}
}
}
}
}
let prevInteractions: Set<Interaction> = (null: any);
if (enableSchedulerTracing) {
// We're about to start new traced work.
// Restore pending interactions so cascading work triggered during the render phase will be accounted for.
prevInteractions = __interactionsRef.current;
__interactionsRef.current = root.memoizedInteractions;
}
let didFatal = false;
startWorkLoopTimer(nextUnitOfWork);
// 上面初始化工作做完之后,就开始 workLoop
// 如果有 catch 就会有一大段处理逻辑, 这里先跳过
//
do {
try {
workLoop(isYieldy);
} catch (thrownValue) {
if (nextUnitOfWork === null) {
// This is a fatal error.
didFatal = true;
onUncaughtError(thrownValue);
} else {
if (__DEV__) {
// Reset global debug state
// We assume this is defined in DEV
(resetCurrentlyProcessingQueue: any)();
}
const failedUnitOfWork: Fiber = nextUnitOfWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
}
// TODO: we already know this isn't true in some cases.
// At least this shows a nicer error message until we figure out the cause.
// https://github.com/facebook/react/issues/12449#issuecomment-386727431
invariant(
nextUnitOfWork !== null,
'Failed to replay rendering after an error. This ' +
'is likely caused by a bug in React. Please file an issue ' +
'with a reproducing case to help us find it.',
);
const sourceFiber: Fiber = nextUnitOfWork;
let returnFiber = sourceFiber.return;
if (returnFiber === null) {
// This is the root. The root could capture its own errors. However,
// we don't know if it errors before or after we pushed the host
// context. This information is needed to avoid a stack mismatch.
// Because we're not sure, treat this as a fatal error. We could track
// which phase it fails in, but doesn't seem worth it. At least
// for now.
didFatal = true;
onUncaughtError(thrownValue);
} else {
throwException(
root,
returnFiber,
sourceFiber,
thrownValue,
nextRenderExpirationTime,
);
nextUnitOfWork = completeUnitOfWork(sourceFiber);
continue;
}
}
}
break;
} while (true);
if (enableSchedulerTracing) {
// Traced work is done for now; restore the previous interactions.
__interactionsRef.current = prevInteractions;
}
// We're done performing work. Time to clean up.
isWorking = false;
ReactCurrentOwner.currentDispatcher = null;
resetContextDependences();
// 在处理完 workLoop 后这里会有各种不同的判断
// Yield back to main thread.
// 这里代表有致命的错误
if (didFatal) {
const didCompleteRoot = false;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
interruptedBy = null;
// There was a fatal error.
if (__DEV__) {
resetStackAfterFatalErrorInDev();
}
// `nextRoot` points to the in-progress root. A non-null value indicates
// that we're in the middle of an async render. Set it to null to indicate
// there's no more work to be done in the current batch.
nextRoot = null;
onFatal(root);
return;
}
// 正常流程走完,这个if一定会匹配
// 因为已经跳出 workLoop 了,说明一定有 react没有意识到的错误,所以调用 onYield
if (nextUnitOfWork !== null) {
// There's still remaining async work in this tree, but we ran out of time
// in the current frame. Yield back to the renderer. Unless we're
// interrupted by a higher priority update, we'll continue later from where
// we left off.
const didCompleteRoot = false;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
interruptedBy = null;
onYield(root);
return;
}
// We completed the whole tree.
const didCompleteRoot = true;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
const rootWorkInProgress = root.current.alternate;
invariant(
rootWorkInProgress !== null,
'Finished root should have a work-in-progress. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// `nextRoot` points to the in-progress root. A non-null value indicates
// that we're in the middle of an async render. Set it to null to indicate
// there's no more work to be done in the current batch.
nextRoot = null;
interruptedBy = null;
// 这里也是
if (nextRenderDidError) {
// There was an error
if (hasLowerPriorityWork(root, expirationTime)) {
// There's lower priority work. If so, it may have the effect of fixing
// the exception that was just thrown. Exit without committing. This is
// similar to a suspend, but without a timeout because we're not waiting
// for a promise to resolve. React will restart at the lower
// priority level.
markSuspendedPriorityLevel(root, expirationTime);
const suspendedExpirationTime = expirationTime;
const rootExpirationTime = root.expirationTime;
onSuspend(
root,
rootWorkInProgress,
suspendedExpirationTime,
rootExpirationTime,
-1, // Indicates no timeout
);
return;
} else if (
// There's no lower priority work, but we're rendering asynchronously.
// Synchronsouly attempt to render the same level one more time. This is
// similar to a suspend, but without a timeout because we're not waiting
// for a promise to resolve.
!root.didError &&
!isExpired
) {
root.didError = true;
const suspendedExpirationTime = (root.nextExpirationTimeToWorkOn = expirationTime);
const rootExpirationTime = (root.expirationTime = Sync);
onSuspend(
root,
rootWorkInProgress,
suspendedExpirationTime,
rootExpirationTime,
-1, // Indicates no timeout
);
return;
}
}
// 注意这里的错误
if (!isExpired && nextLatestAbsoluteTimeoutMs !== -1) {
// The tree was suspended.
const suspendedExpirationTime = expirationTime;
markSuspendedPriorityLevel(root, suspendedExpirationTime);
// Find the earliest uncommitted expiration time in the tree, including
// work that is suspended. The timeout threshold cannot be longer than
// the overall expiration.
const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
root,
expirationTime,
);
const earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime);
if (earliestExpirationTimeMs < nextLatestAbsoluteTimeoutMs) {
nextLatestAbsoluteTimeoutMs = earliestExpirationTimeMs;
}
// Subtract the current time from the absolute timeout to get the number
// of milliseconds until the timeout. In other words, convert an absolute
// timestamp to a relative time. This is the value that is passed
// to `setTimeout`.
const currentTimeMs = expirationTimeToMs(requestCurrentTime());
let msUntilTimeout = nextLatestAbsoluteTimeoutMs - currentTimeMs;
msUntilTimeout = msUntilTimeout < 0 ? 0 : msUntilTimeout;
// TODO: Account for the Just Noticeable Difference
const rootExpirationTime = root.expirationTime;
onSuspend(
root,
rootWorkInProgress,
suspendedExpirationTime,
rootExpirationTime,
msUntilTimeout,
);
return;
}
// Ready to commit.
onComplete(root, rootWorkInProgress, expirationTime);
}
- renderRoot 代码会相对比较长,要把代码的区块进行一个区分
- 一些原版英文注释,和我添加的中文注释如上
现在来看下 workLoop 的源码
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
- 它接收一个 isYieldy 作为参数
- 这个参数意味着是否可以被中断
- Sync的任务和已超时的异步任务都是不可中断的
- 如果是不可中断的,只要有
nextUnitOfWork
- 就会继续调用
performUnitOfWork
- 如果是可以中断的,就通过判断
!shouldYield()
- 来看当前时间片中是否还有足够的时间继续渲染下一个节点
再来看下 performUnitOfWork
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
startWorkTimer(workInProgress);
if (__DEV__) {
ReactCurrentFiber.setCurrentFiber(workInProgress);
}
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
stashedWorkInProgressProperties = assignFiberPropertiesInDEV(
stashedWorkInProgressProperties,
workInProgress,
);
}
let next;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
if (workInProgress.mode & ProfileMode) {
// Record the render duration assuming we didn't bailout (or error).
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
}
} else {
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
}
if (__DEV__) {
ReactCurrentFiber.resetCurrentFiber();
if (isReplayingFailedUnitOfWork) {
// Currently replaying a failed unit of work. This should be unreachable,
// because the render phase is meant to be idempotent, and it should
// have thrown again. Since it didn't, rethrow the original error, so
// React's internal stack is not misaligned.
rethrowOriginalError();
}
}
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress);
}
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
return next;
}
- 它声明了一个 next 变量,next = beginWork(…), 这里涉及到对每个节点的更新
- 更新完一个节点之后,它会返回它的下一个节点
- 会更新 workInProgress.memoizedProps, 节点已经更新完了
- 最新的 props 已经变成目前正在用的 props
- 先跳过
- 跳过 DEV 的代码
- 如果 next === null 说明这个节点已经更新到子树的叶子节点了
- 这棵子树就可以结束了
- 结束就调用 completeUnitOfWork
- 它也会返回它的下一个节点
- 最后,return next
- 在 workLoop 函数中可看到,它会赋值给 nextUnitOfWork
// 参考其中一个 while while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }
- 所以,真正到 nextUnitOfWork 为 null 的情况是它到了根节点,即 FiberRoot 节点
- 它的 return 是 null,这时就跳出了 while 循环了
- 在 workLoop 函数中可看到,它会赋值给 nextUnitOfWork