2021SC@SDUSC
类似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的处理留在具体功能实现时讲解。
和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树。
至此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阶段工作流程。
流程图来源于网络,作者看到请和我联系。