React源码解析————Render阶段(三)
2021SC@SDUSC
2021SC@SDUSC
CompleteWork
流程预览
类似beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
// ...省略
我们重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点),其他类型Fiber的处理留在具体功能实现时讲解。
HostComponent
和beginWork一样,我们根据current === null ?判断是mount还是update。
同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
const currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext,
)
) {
// If changes to the hydrated node need to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:
1.onClick、onChange等回调函数的注册
2.处理style prop
3.处理DANGEROUSLY_SET_INNER_HTML prop
4.处理children prop
我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
我们可以从这里看到updateHostComponent方法定义。
在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// In mutation mode, this is sufficient for a bailout because
// we won't touch this node even if children changed.
return;
}
// If we get updated because one of our children updated, we don't
// have newProps so we'll have to reuse them.
// TODO: Split the update API as separate for the props vs. children.
// Even better would be if children weren't special cased at all tho.
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// TODO: Experiencing an error where oldProps is null. Suggests a host
// component is hitting the resume path. Figure out why. Possibly
// related to `hidden`.
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
// TODO: Type this specific to this type of component.
workInProgress.updateQueue = (updatePayload: any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update. All the work is done in commitWork.
if (updatePayload) {
markUpdate(workInProgress);
}
};
其中updatePayload为数组形式,他的奇数索引的值为变化的prop key,偶数索引的值为变化的prop value。
当mount时,同样,我们省略了不相关的逻辑。可以看到,mount时的主要逻辑包括三个:
1.为Fiber节点生成对应的DOM节点
2.将子孙DOM节点插入刚生成的DOM节点中
3.与update逻辑中的updateHostComponent类似的处理props的过程
// mount的情况
// ...省略服务端渲染相关逻辑
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
还记得我们讲到:mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?
原因就在于completeWork中的appendAllChildren方法。
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。
completeUnitOfWork
至此render阶段的绝大部分工作就完成了。
但是还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?
这显然是很低效的。
为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。
//完成当前节点的 work,然后移动到兄弟节点,重复该操作,当没有更多兄弟节点时,返回至父节点
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
//从下至上,移动到该节点的兄弟节点,如果一直往上没有兄弟节点,就返回父节点
//可想而知,最终会到达 root 节点
workInProgress = unitOfWork;
do {
// 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;
//获取父节点
const returnFiber = workInProgress.return;
// Check if the work completed or if something threw.
//判断节点的操作是否完成,还是有异常丢出
//Incomplete表示捕获到该节点抛出的 error
//&是表示位的与运算,把左右两边的数字转化为二进制,然后每一位分别进行比较,如果相等就为1,不相等即为0
//如果该节点没有异常抛出的话,即可正常执行
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
//dev 环境,可不看
setCurrentDebugFiberInDEV(workInProgress);
let next;
//如果不能使用分析器的 timer 的话,直接执行completeWork,
//否则执行分析器timer,并执行completeWork
if (
!enableProfilerTimer ||
(workInProgress.mode & ProfileMode) === NoMode
) {
//完成该节点的更新
next = completeWork(current, workInProgress, renderExpirationTime);
} else {
//启动分析器的定时器,并赋成当前时间
startProfilerTimer(workInProgress);
//完成该节点的更新
next = completeWork(current, workInProgress, renderExpirationTime);
// Update render duration assuming we didn't error.
//在没有报错的前提下,更新渲染持续时间
//记录分析器的timer的运行时间间隔,并停止timer
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
}
//停止 work 计时,可不看
stopWorkTimer(workInProgress);
//dev 环境,可不看
resetCurrentDebugFiberInDEV();
//更新该节点的 work 时长和子节点的 expirationTime
resetChildExpirationTime(workInProgress);
//如果next存在,则表示产生了新 work
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
//返回 next,以便执行新 work
return next;
}
//如果父节点存在,并且其 Effect 链没有被赋值的话
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
//子节点的完成顺序会影响副作用的顺序
//如果父节点没有挂载firstEffect的话,将当前节点的firstEffect赋值给父节点的firstEffect
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
//同上,根据当前节点的lastEffect,初始化父节点的lastEffect
if (workInProgress.lastEffect !== null) {
//如果父节点的lastEffect有值的话,将nextEffect赋值
//目的是串联Effect链
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
// If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if needed,
// by doing multiple passes over the effect list. We don't want to
// schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.
//获取副作用标记
const effectTag = workInProgress.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect
// list. PerformedWork effect is read by React DevTools but shouldn't be
// committed.
//如果该副作用标记大于PerformedWork
if (effectTag > PerformedWork) {
//当父节点的lastEffect不为空的时候,将当前节点挂载到父节点的副作用链的最后
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
//否则,将当前节点挂载在父节点的副作用链的头-firstEffect上
returnFiber.firstEffect = workInProgress;
}
//无论父节点的lastEffect是否为空,都将当前节点挂载在父节点的副作用链的lastEffect上
returnFiber.lastEffect = workInProgress;
}
}
}
//如果该 fiber 节点未能完成 work 的话(报错)
else {
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
//节点未能完成更新,捕获其中的错误
const next = unwindWork(workInProgress, renderExpirationTime);
// Because this fiber did not complete, don't reset its expiration time.
//由于该 fiber 未能完成,所以不必重置它的 expirationTime
if (
enableProfilerTimer &&
(workInProgress.mode & ProfileMode) !== NoMode
) {
// Record the render duration for the fiber that errored.
//记录分析器的timer的运行时间间隔,并停止timer
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
// Include the time spent working on failed children before continuing.
//虽然报错了,但仍然会累计 work 时长
let actualDuration = workInProgress.actualDuration;
let child = workInProgress.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
}
//如果next存在,则表示产生了新 work
if (next !== null) {
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
// TODO: The name stopFailedWorkTimer is misleading because Suspense
// also captures and restarts.
//停止失败的 work 计时,可不看
stopFailedWorkTimer(workInProgress);
//更新其 effectTag,标记是 restart 的
next.effectTag &= HostEffectMask;
//返回 next,以便执行新 work
return next;
}
//停止 work 计时,可不看
stopWorkTimer(workInProgress);
//如果父节点存在的话,重置它的 Effect 链,标记为「未完成」
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its effect list.
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
//获取兄弟节点
const siblingFiber = workInProgress.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
}
// Otherwise, return to the parent
//如果能执行到这一步的话,说明 siblingFiber 为 null,
//那么就返回至父节点
workInProgress = returnFiber;
} while (workInProgress !== null);
// We've reached the root.
//当执行到这里的时候,说明遍历到了 root 节点,已完成遍历
//更新workInProgressRootExitStatus的状态为「已完成」
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
该代码的作用是完成当前节点的work,并赋值Effect链,然后移动到兄弟节点,重复该操作,当没有更多兄弟节点时,返回至父节点,最终返回至root节点
整体上看是一个大的while循环:
从当前节点开始,遍历到兄弟节点,当无兄弟节点时,返回至父节点,
再从父节点开始,遍历到兄弟节点,当无兄弟节点时,返回至父父节点,
可想而知,最终会返回至rootFiber节点
EffectList的赋值:
假设Span1有更新,Span2也有更新
那么父节点DIV的firstEffect和lastEffect在Span1执行completeUnitOfWork()后,会是下面这个样子:
workInProgress1即表示Span1对应的fiber对象
当轮到Span2执行completeUnitOfWork()后,又会变成下面这个样子:
也就是说:Effect链是帮助父节点简单判断子节点是否有更新及更新顺序的。
小结
至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。
流程图来源于网络,作者看到请和我联系。