深度解析React-render阶段:递与归
对于 React 的 render 阶段可以分为两个部分,递和归。
这里我将从createRoot().render
方法开始切入。这也是 React 18 的一个更新点。以前是通过 ReactDOM.render 去挂在 react 的框架,现在要先 createRoot,然后再执行 render 方法。最主要的是对于事件系统的更新,而不去污染 Document 元素。
# React 的优先级机制
React 渲染阶段包含三个优先级机制,分别是:
- React 的 Lane 优先级
- React 的事件优先级
- Scheduler 的优先级
React 的 Lane 使用二进制表示,共有 31 个值,通过位运算判断优先级,这提高了性能。Lane 优先级主要用于 React 实现一些 API 时的调度。
React 的事件优先级分为 5 个,是开发者在使用 React 时涉及最多的优先级机制。在触发事件回调时,根据事件类型分配不同的优先级。这些事件优先级和 Scheduler 的优先级可以互相转换。
Scheduler 的优先级是用于调度使用,具体可以看调度器文章。
ReactFiberWorkLoop.js - requestUpdateLane方法
// Updates originating inside certain React methods, like flushSync, have
// their priority set by tracking it with a context variable.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
# 更新
当 React 涉及更新的时候,React 会创建一个 update 的对象,并 lane 优先级赋予给 fiber(当然 fiber 会取最高的优先级),最终通过 ensureRootIsScheduled
和 scheduleTaskForRootDuringMicrotask
函数进入调度器进行任务调度。
调度器会通过优先级判断,进入 performConcurrentWorkOnRoot
,根据情况选择是使用 Concurrent 模式还是 Sync 模式进行虚拟 DOM 的“递归”操作。
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
主要关注 includesBlockingLane
函数,用于判断是否进入 Concurrent 模式。对于用户的一些离散事件,会通过 Sync 模式快速响应。只有遇到 startTransitoin
, useTranstiion
, **useDeferredValue
**这些函数的时候才会进入 Concurrent 模式,而 Concurrent 模式下,会以 5ms 进行时间分片,如果超过 5ms 的计算,就会去判断是否有其他更高优先级的任务。
export function includesBlockingLane(root: FiberRoot, lanes: Lanes): boolean {
if (
allowConcurrentByDefault &&
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
) {
// Concurrent updates by default always use time slicing.
return false;
}
const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane; // React的事件优先级均属于这几个事件
return (lanes & SyncDefaultLanes) !== NoLanes;
}
不管最后进入 Concurrent 模式还是 Sync 模式,都会进入 workLoop
,这是 render 的“递”入口。
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
performUnitOfWork(workInProgress);
}
}
# 递
在 workLoop
中,通过 performUnitOfWork
进行递归操作。在每次循环中,调用 beginWork
,该函数首先判断当前 Fiber 的新旧 props 是否改变,以及 context 是否改变。如果没有改变,设置 didReceiveUpdate
为 false。然后根据当前 Fiber 的 tag 属性进入不同类型的方法中处理。
在每个类型方法的处理函数中,判断是更新还是挂载( current ≠== null
)。如果 didReceiveUpdate
为 false,则执行 bailoutOnAlreadyFinishedWork
,标记这个 Fiber 没有更新,并克隆它的 child 节点进行递操作。
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
markSkippedUpdateLanes(workInProgress.lanes);
// Check if the children have any pending work.
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
if (enableLazyContextPropagation && current !== null) {
// Before bailing out, check if there are any context changes in
// the children.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
} else {
return null;
}
}
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
如果有存在更新,则会执行 reconcileChildren
函数的操作。
这个函数也是 React 的 render 阶段中最为重要的方法。先判断是挂载还是更新,两者的区别在于,挂载不会执行 delete(删除),place(替换)等操作。
接下来 React 的核心diff 算法来处理 React 的渲染,给每个涉及修改的 Fiber 都打上 flag,用于后续 commit 的时候一起执行真实 DOM 树的更新。
当整棵树遍历完成后,beginWork
将返回 null,表示 render 的递操作完成,进入 completeWork
函数,这是 render 阶段的归操作。
# 归
在递操作,也就是 beginWork
中,是从 root 节点开始往底部遍历,而归操作中,completeWork
则从底部开始往 Root 节点返回。
该函数较为简单,主要执行 bubbleProperties
函数,将优先级和子树打上的 flag 同步到父节点中,一直到 Root 节点。
这样有助于后续的 commit 进行树遍历时,知道哪颗子树需要更新,哪个不需要。
ps: 在以前的版本中,会形成一个链表,在 completeWork
中遍历链表执行 commit 操作。
function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;
let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;
if (!didBailout) {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let actualDuration = completedWork.actualDuration;
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
// When a fiber is cloned, its actualDuration is reset to 0. This value will
// only be updated if work is done on the fiber (i.e. it doesn't bailout).
// When work is done, it should bubble to the parent's actualDuration. If
// the fiber has not been cloned though, (meaning no work was done), then
// this value will reflect the amount of time spent working on a previous
// render. In that case it should not bubble. We determine whether it was
// cloned by comparing the child pointer.
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
actualDuration += child.actualDuration;
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
child.return = completedWork;
child = child.sibling;
}
}
completedWork.subtreeFlags |= subtreeFlags;
} else {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;
// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
child.return = completedWork;
child = child.sibling;
}
}
completedWork.subtreeFlags |= subtreeFlags;
}
completedWork.childLanes = newChildLanes;
return didBailout;
}
在执行完“递归”操作后,React 的 Render 阶段结束,接下来进入 commit 阶段,这个阶段是无法被打断的。