blog
blog copied to clipboard
renderRoot
上一篇解析了React
在进入work
流程之前会把产生了更新的FiberRoot
通过addRootToSchedule
添加到一个调度队列中去,这个调度队列是一个环形的链表。之后在performWork
方法中,递归的调用findHighestPriorityRoot
方法找到队列中expirationTime
最大的那个fiberRoot
,并将expirationtime
为noWork
的fiberRoot
从队列中剔除,并将这个FiberRoot
设置为全局变量nextFlushedRoot
,将优先级最高的expirationTime
设置为全局变量nextFlushedExpirationTime
,之后调用performWorkOnRoot
方法进入renderRoot
流程。
/**
* 渲染FiberRoot节点
*
* @param {FiberRoot} root 需要被渲染的FiberFRoot
* @param {boolean} isYieldy 任务是否可以被中断
* @returns {void}
*/
function renderRoot(root: FiberRoot, isYieldy: boolean): void {
invariant(
!isWorking,
"renderRoot was called recursively. This error is likely caused " +
"by a bug in React. Please file an issue."
);
flushPassiveEffects();
// 标记当前正在进行render工作
isWorking = true;
// reactHooks相关
if (enableHooks) {
ReactCurrentOwner.currentDispatcher = Dispatcher;
} else {
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
}
// 获取root的计算出来优先级最高的expirationTime,
// 这个时间是在scheduleWork的过程中通过findNextExpirationTimeToWorkOn比较出来的
// 因此这个时间既可能是本次更新的expirationTime,
// 也可能是之前在提交中被中断的任务的expirationTime(或者其他情况)
const expirationTime = root.nextExpirationTimeToWorkOn;
// Check if we're starting from a fresh stack, or if we're resuming from
// previously yielded work.
// 判断此次更新是一个全新的任务栈,还是恢复之前被中断的任务
// 如果是一个全新的任务,进入创建workInProgress流程
if (
expirationTime !== nextRenderExpirationTime ||
root !== nextRoot ||
nextUnitOfWork === null
) {
// Reset the stack and start working from the root.
// 重置更新栈,从当前root重新开始
resetStack();
nextRoot = root;
nextRenderExpirationTime = expirationTime;
// fiber节点的的alternate就是nextUnitOfWork
// 这里就是为fiber节点创建alternate
nextUnitOfWork = createWorkInProgress(
nextRoot.current,
null,
nextRenderExpirationTime
);
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);
do {
try {
workLoop(isYieldy);
} catch (thrownValue) {
resetContextDependences();
resetHooks();
// Reset in case completion throws.
// This is only used in DEV and when replaying is on.
let mayReplay;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
mayReplay = mayReplayFailedUnitOfWork;
mayReplayFailedUnitOfWork = true;
}
if (nextUnitOfWork === null) {
// This is a fatal error.
didFatal = true;
onUncaughtError(thrownValue);
} else {
if (enableProfilerTimer && nextUnitOfWork.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(nextUnitOfWork, true);
}
if (__DEV__) {
// Reset global debug state
// We assume this is defined in DEV
(resetCurrentlyProcessingQueue: any)();
}
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
if (mayReplay) {
const failedUnitOfWork: Fiber = nextUnitOfWork;
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();
resetHooks();
// 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 (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 &&
isYieldy
) {
root.didError = true;
const suspendedExpirationTime = (root.nextExpirationTimeToWorkOn = expirationTime);
const rootExpirationTime = (root.expirationTime = Sync);
onSuspend(
root,
rootWorkInProgress,
suspendedExpirationTime,
rootExpirationTime,
-1 // Indicates no timeout
);
return;
}
}
if (isYieldy && 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
首先会标记isWorking
为true
,之后读取root
节点上的nextExpirationTimeToWorkOn
作为本次渲染的expirationTime
,nextExpirationTimeToWorkOn
并不一定就是root
上此次更新的expirationTime
,也有可能是之前被中断的更新任务。然后为fiberRoot
的fiber
节点创建alternate
对象,并赋值给全局变量nextUnitOfWork
,fiberRoot
的更新变动将体现在alternate
对象上。
准备工作完成,进入workLoop
。workLoop
流程中会递归的完成整颗Fiber
树的构建,performUnitOfWork
将返回每一个fiber
节点,并且这个fiber
节点将作为下一次performUnitOfWork
的入参,这里仅分析第一次HostRoot
的处理。
/**
*
*
* @param {*} isYieldy 任务是否能够被中断
*/
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until there's a higher priority event
while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
调用performUnitOfWork
,并将前面设置的全局变量nextUnitOfWork
也就是fiberRoot
对象作为参数传入。
/**
* 主要是性能检测,最终的工作是调用beginWork
*
* @param {Fiber} workInProgress 当前处于工作流程中的fiber节点
* @returns {(Fiber | null)}
*/
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.
// 获取alternate对应的fiber节点
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
startWorkTimer(workInProgress);
if (__DEV__) {
setCurrentFiber(workInProgress);
}
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
stashedWorkInProgressProperties = assignFiberPropertiesInDEV(
stashedWorkInProgressProperties,
workInProgress
);
}
let next;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
// 开始render流程
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__) {
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;
// next是一次work后产生的fiber节点,被返回后作为下一次performUnitOfWork的入参
return next;
}