React源码学习进阶(六)completeWork究竟做了什么
本文采用React v16.13.1版本源码进行分析
源码解析
之前提到,在beginWork
走到叶子节点,没有下一个子节点的时候,就会走到completeWork
的流程:
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 这里没有子节点了,走completeUnitOfWork
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
其中completeUnitOfWork
逻辑如下:
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.
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.
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
setCurrentDebugFiberInDEV(workInProgress);
let next;
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.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
}
stopWorkTimer(workInProgress);
resetCurrentDebugFiberInDEV();
resetChildExpirationTime(workInProgress);
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
return next;
}
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.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
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.
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
} 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.
if (
enableProfilerTimer &&
(workInProgress.mode & ProfileMode) !== NoMode
) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
// Include the time spent working on failed children before continuing.
let actualDuration = workInProgress.actualDuration;
let child = workInProgress.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
}
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.
stopFailedWorkTimer(workInProgress);
next.effectTag &= HostEffectMask;
return next;
}
stopWorkTimer(workInProgress);
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
workInProgress = returnFiber;
} while (workInProgress !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
这块逻辑初看有一些复杂,其实整个逻辑就是一个do...while
:
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.
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;
next = completeWork(current, workInProgress, renderExpirationTime);
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
workInProgress = returnFiber;
} while (workInProgress !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
简化一下中间的逻辑,这里就是两个点:
对当前的
workInProgress
执行completeWork
逻辑遍历兄弟节点,如果有兄弟节点,则直接返回(意味着开启新一轮的
performUnitWork
),否则对父节点继续进行completeWork
的流程。
接下来看一看completeWork
的逻辑:
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): 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: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
return null;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
}
}
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (enableDeprecatedFlareAPI) {
const prevListeners = current.memoizedProps.DEPRECATED_flareListeners;
const nextListeners = newProps.DEPRECATED_flareListeners;
if (prevListeners !== nextListeners) {
markUpdate(workInProgress);
}
}
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.
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.
let 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);
}
if (enableDeprecatedFlareAPI) {
const listeners = newProps.DEPRECATED_flareListeners;
if (listeners != null) {
updateDeprecatedEventListeners(
listeners,
workInProgress,
rootContainerInstance,
);
}
}
} else {
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
// This needs to be set before we mount Flare event listeners
workInProgress.stateNode = instance;
if (enableDeprecatedFlareAPI) {
const listeners = newProps.DEPRECATED_flareListeners;
if (listeners != null) {
updateDeprecatedEventListeners(
listeners,
workInProgress,
rootContainerInstance,
);
}
}
// 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);
}
}
return null;
}
case HostText: {
let newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText(current, workInProgress, oldText, newText);
} else {
if (typeof newText !== 'string') {
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.
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
return null;
}
// ... 省略很多不关注的分支
case Block:
if (enableBlocksAPI) {
return null;
}
break;
}
}
实际上可以看到,绝大多数组件的completeWork
都是没有做逻辑的,我们核心关注下HostComponent
的实现:
调用
createInstance
,创建了一个DOM元素调用
appendAllChildren
,将所有的子节点加入进来将元素存入
Fiber
节点的stateNode
当中
首先来看一下createInstance
,它位于packages/react-dom/src/client/ReactDOMHostConfig.js
:
xport function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev = ((hostContext: any): HostContextDev);
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
const string = '' + props.children;
const ownAncestorInfo = updatedAncestorInfo(
hostContextDev.ancestorInfo,
type,
);
validateDOMNesting(null, string, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
} else {
parentNamespace = ((hostContext: any): HostContextProd);
}
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
其实可以理解为就是调用document.createElement
去创建了对应的dom
元素。
接着看一下appendAllChildren
的实现
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
这里的appendInitialChild
其实可以理解为appendChild
的实现,这段代码也就是将它的所有子节点,如果是DOM元素的全部append
进来。
最后我们可以想象得到,在completeWork
的阶段,Fiber
树的索引顺序是从叶子节点往上遍历,因此整个DOM
树其实是从completeWork
开始逐步建立起来,最后索引到根节点为止。
小结一下
其实completeWork
并没有做什么事情,重点是将DOM树在遍历过程中组建起来(但并未真正挂载),在后续的commit
过程中进行真正的渲染挂载,它的整个遍历顺序可以通过下图来总结:
可以看到,beginWork
的是一个从上至下,从左至右的深度遍历顺序,而completeWork
更像是一个从叶子节点由左至右,由下至上的遍历过程,比较类似于后续遍历的方式。
在beginWork
的时候我们从上至下进行的是reconciler
流程,在completeWork
的时候,我们是从下至上构建出DOM树。
最后用户能感知到的更新是统一进入commit
阶段处理,在这个阶段一定是同步执行的,不可以被打断。这块内容下篇文章会详细分析。
Last updated