React Reconciler Render阶段之completeWork理解

Tags
Published
Author
上一篇可以了解到,组件执行beginWork后会创建子Fiber节点,节点上可能存在Effects List
方法调用栈如下: performUnitOfWork –> beginWork–> updateClassComponent –> finishedComponent –> completeUnitOfWork
performUnitOfWork方法中,如果遍历完创建了所有的Fiber节点,就进行completeUnitOfWork方法,创建对应的DOM节点。
if (next === null) { // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); }
接下来看一下 completeUnitOfWork方法
function completeUnitOfWork(unitOfWork: Fiber): void { //准备完成当前Fiber节点的工作,完成之后就准备完成下一个兄弟Fiber节点的工作,如果没有兄弟Fiber节点,那就回到父Fiber节点继续工作。 let completedWork = unitOfWork; do { const current = completedWork.alternate; const returnFiber = completedWork.return; // Check if the work completed or if something threw. if ((completedWork.flags & Incomplete) === NoFlags) { setCurrentDebugFiberInDEV(completedWork); let next; if ( !enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode ) { next = completeWork(current, completedWork, subtreeRenderLanes); } else { startProfilerTimer(completedWork); next = completeWork(current, completedWork, subtreeRenderLanes); // Update render duration assuming we didn't error. stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); } resetCurrentDebugFiberInDEV(); if (next !== null) { // Completing this fiber spawned new work. Work on that next. workInProgress = next; return; } resetChildLanes(completedWork); if ( returnFiber !== null && // Do not append effects to parents if a sibling failed to complete (returnFiber.flags & Incomplete) === NoFlags ) { // 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. if (returnFiber.firstEffect === null) { returnFiber.firstEffect = completedWork.firstEffect; } if (completedWork.lastEffect !== null) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork.firstEffect; } returnFiber.lastEffect = completedWork.lastEffect; } const flags = completedWork.flags; // Skip both NoWork and PerformedWork tags when creating the effect // list. PerformedWork effect is read by React DevTools but shouldn't be // committed. if (flags > PerformedWork) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork; } else { returnFiber.firstEffect = completedWork; } returnFiber.lastEffect = completedWork; } } } 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(completedWork, subtreeRenderLanes); // Because this fiber did not complete, don't reset its expiration time. 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. next.flags &= HostEffectMask; workInProgress = next; return; } if ( enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode ) { // Record the render duration for the fiber that errored. stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); // Include the time spent working on failed children before continuing. let actualDuration = completedWork.actualDuration; let child = completedWork.child; while (child !== null) { actualDuration += child.actualDuration; child = child.sibling; } completedWork.actualDuration = actualDuration; } if (returnFiber !== null) { // Mark the parent fiber as incomplete and clear its subtree flags. returnFiber.flags |= Incomplete; returnFiber.subtreeFlags = NoFlags; returnFiber.deletions = null; } } const siblingFiber = completedWork.sibling; if (siblingFiber !== null) { // If there is more work to do in this returnFiber, do that next. workInProgress = siblingFiber; return; } // Otherwise, return to the parent completedWork = returnFiber; // Update the next thing we're working on in case something throws. workInProgress = completedWork; } while (completedWork !== null); // We've reached the root. if (workInProgressRootExitStatus === RootIncomplete) { workInProgressRootExitStatus = RootCompleted; }}
completeWork
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: bubbleProperties(workInProgress); return null; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } bubbleProperties(workInProgress); return null; } case HostRoot: { const fiberRoot = (workInProgress.stateNode: FiberRoot); //... updateHostContainer(current, workInProgress); bubbleProperties(workInProgress); return null; } case HostComponent: {//...} case HostText: {//...} case SuspenseComponent: {//...}
这里先讲解页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点
beginWork一样,根据current === null来 判断是mount还是update
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; 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时

updateHostComponent
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); } };
如果是update并且此workInProgress Fiber节点对应的原生DOM节点存在,就不需要生成DOM节点,只需要对props进行处理就行了。这里的props主要是:
  • onClickonChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop
被处理完props会被存入workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。
import React, { useState } from "./react";import ReactDOM from "./react-dom";const rootElement = document.getElementById("root");function App() { const [count, updateCount] = useState(0); return ( <h3 title={count} dev={count} onClick={() => updateCount(count + 1)}> <p>hello word</p> </h3> );}window.ReactDOM.render(<App />, rootElement);
点击区域,更新count的值,h3的props发生改变,改变内容为updatePayload
其中updatePayload数组形式,他的偶数索引的值为变化的prop key奇数索引的值为变化的prop value
notion image
updatePayload

mount时

// 为Fiber创建对应DOM节点const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); // 将子孙DOM节点插入刚生成的DOM节点中 appendAllChildren(instance, workInProgress, false, false); // DOM节点赋值给fiber.stateNode workInProgress.stateNode = instance;//处理props if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }
completeWork 会从子Fiber节点归到rootFiber节点,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么递归到rootFiber时,我们已经有一个构建好的离屏DOM树
构建完DOM树后,render阶段全部工作完成。在performSyncWorkOnRoot函数fiberRootNode被传递给mommitRoot方法,开启commit阶段工作流程。
所以completeWork工作流程如下:
notion image
completedWork