本文采用React v16.13.1版本源码进行分析
源码解析
首先我们回顾一下挂载开始的入口performSyncWorkOnRoot
,它位于packages/react-reconciler/src/ReactFiberWorkLoop.js
:
function performSyncWorkOnRoot(root) {
// Check if there's expired work on this root. Otherwise, render at Sync.
const lastExpiredTime = root.lastExpiredTime;
const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
flushPassiveEffects();
// If the root or expiration time have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
}
// If we have a work-in-progress fiber, it means there's still work to do
// in this root.
if (workInProgress !== null) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher(root);
const prevInteractions = pushInteractions(root);
startWorkLoopTimer(workInProgress);
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
if (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
}
if (workInProgressRootExitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
stopInterruptedWorkLoopTimer();
prepareFreshStack(root, expirationTime);
markRootSuspendedAtTime(root, expirationTime);
ensureRootIsScheduled(root);
throw fatalError;
}
if (workInProgress !== null) {
// This is a sync render, so we should have finished the whole tree.
invariant(
false,
'Cannot commit an incomplete root. This error is likely caused by a '
'bug in React. Please file an issue.',
);
} else {
// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
stopFinishedWorkLoopTimer();
root.finishedWork = (root.current.alternate: any);
root.finishedExpirationTime = expirationTime;
finishSyncRender(root);
}
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
ensureRootIsScheduled(root);
}
return null;
}
这里在执行完workLoopSync
循环后,最终会走到finishSyncRender
的逻辑:
function finishSyncRender(root) {
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
commitRoot(root);
}
代码语言:javascript复制function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediatePriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
最终会走到commitRootImpl
方法里,值得注意的是这个流程是不可以被打断的。
commitRootImpl
的实现非常的长,为方便说明,这里对挂载时的核心代码进行截取:
function commitRootImpl(root, renderPriorityLevel) {
const finishedWork = root.finishedWork;
const expirationTime = root.finishedExpirationTime;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
// commitRoot never returns a continuation; it always finishes synchronously.
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
root.nextKnownPendingLevel = NoWork;
// Get the list of effects.
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
// A fiber's effect list consists only of its children, not itself. So if
// the root has an effect, we need to add it to the end of the list. The
// resulting list is the set that would belong to the root's parent, if it
// had one; that is, all the effects in the tree including the root.
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}
if (firstEffect !== null) {
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
const prevInteractions = pushInteractions(root);
// Reset this to null before calling lifecycles
ReactCurrentOwner.current = null;
// The commit phase is broken into several sub-phases. We do a separate pass
// of the effect list for each phase: all mutation effects come before all
// layout effects, and so on.
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
startCommitSnapshotEffectsTimer();
prepareForCommit(root.containerInfo);
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(null, commitBeforeMutationEffects, null);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitBeforeMutationEffects();
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(
null,
commitMutationEffects,
null,
root,
renderPriorityLevel,
);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
resetAfterCommit(root.containerInfo);
// The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork;
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(
null,
commitLayoutEffects,
null,
root,
expirationTime,
);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitLayoutEffects(root, expirationTime);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
stopCommitLifeCyclesTimer();
nextEffect = null;
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
requestPaint();
executionContext = prevExecutionContext;
} else {
// No effects.
root.current = finishedWork;
}
// If layout work was scheduled, flush it now.
flushSyncCallbackQueue();
return null;
}
这里核心做了几件事情:
- 拿到
firstEffect
进行处理 - 执行
commitBeforeMutationEffects
,主要触发classComponent
的getSnapshotBeforeUpdate
钩子 - 执行
commitMutationEffects
,这里是核心的DOM操作 - 执行
commitLayoutEffects
,这里主要执行生命周期钩子,比如componentDidMount
、componentDidUpdate
等。
首先,我们分析一下firstEffect
是怎么产生的,目前firstEffect
来自于root.finishedWork
,这个work是在performSyncWorkOnRoot
被赋值:
root.finishedWork = (root.current.alternate: any);
也就是root
的workInProgress
对应的Fiber,那这个Fiber
上的firstEffect
来自哪里呢?在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) {
// ...
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;
}
}
}
// ...
}
当workInProgress
的effectTag
要大于PerformedWork
的时候,会给它的returnFiber
加上effectList
,实际上就是子Fiber节点为父fiber节点标记effect
的过程。这个时候root下的子节点effectTag
为Placement
,,所root
节点的firstEffect
指向了这个子节点。
那这个effectTag
又是什么时候被标记的呢?对于挂载阶段来说,还记得在beginWork
的阶段,我们有一个placeSingleChild
的逻辑:
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
}
}
// ...
}
function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement;
}
return newFiber;
}
这个placeSingleChild
会在shouldTrackSideEffects
为true
并且alternate
为空的时候,把effectTag
标记成Placement
。shouldTrackSideEffects
变量之前我们分析过,是在reconcileChildFibers
的时候被赋值:
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
可以看到,在current
为null
的时候,走mountChildFibers
逻辑,而有current
则走reconcileChildFibers
逻辑,在我们进行初始挂载的时候,其实只有root
节点的current
是存在的,所以只有root
节点的第一个子节点,才会被标记为Placement
的effectTag
。
在这里初次挂载的逻辑在这里就很清晰了,root第一个子节点被标记为Placement
,作为firstEffect
进行接下来的流程。
后续流程,我们重点关注commitMutationEffects
,它是真正去执行Effect
操作的地方,位于packages/react-reconciler/src/ReactFiberWorkLoop.js
:
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
let primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
// TODO: Only record a mutation effect if primaryEffectTag is non-zero.
recordEffect();
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
对于初次挂载来说,我们重点关注commitPlacement
的实现,它位于packages/react-reconciler/src/ReactFiberCommitWork.js
:
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case FundamentalComponent:
if (enableFundamentalAPI) {
parent = parentStateNode.instance;
isContainer = false;
}
// eslint-disable-next-line-no-fallthrough
default:
invariant(
false,
'Invalid host parent fiber. This error is likely caused by a bug '
'in React. Please file an issue.',
);
}
if (parentFiber.effectTag & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.effectTag &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
对于初次挂载来说,最终会挂载到根节点的container
下:
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
const stateNode = isHost ? node.stateNode : node.stateNode.instance;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
这里就是一个向下递归遍历的过程,找到Host
节点为止,将Host
的元素插入到parent
当中,最后会走到appendChildToContainer
的逻辑,源码位于packages/react-dom/src/client/ReactDOMHostConfig.js
:
export function appendChildToContainer(
container: Container,
child: Instance | TextInstance,
): void {
let parentNode;
if (container.nodeType === COMMENT_NODE) {
parentNode = (container.parentNode: any);
parentNode.insertBefore(child, container);
} else {
parentNode = container;
parentNode.appendChild(child);
}
// This container might be used for a portal.
// If something inside a portal is clicked, that click should bubble
// through the React tree. However, on Mobile Safari the click would
// never bubble through the *DOM* tree unless an ancestor with onclick
// event exists. So we wouldn't see it and dispatch it.
// This is why we ensure that non React root containers have inline onclick
// defined.
// https://github.com/facebook/react/issues/11918
const reactRootContainer = container._reactRootContainer;
if (
(reactRootContainer === null || reactRootContainer === undefined) &&
parentNode.onclick === null
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((parentNode: any): HTMLElement));
}
}
至此,整个DOM已经操作完成,在后续的链路中去触发React组件的生命周期。
小结一下
commit
阶段本身是同步执行的,在这个阶段主要是拿到effect
的链表,依次执行以下三个步骤:
- 执行
commitBeforeMutationEffects
,执行渲染前的声明周期钩子。 - 执行
commitMutationEffects
,处理Effect。 - 执行
commitLayoutEffects
,执行生命周期钩子。
而Effect来源于beginWork
和completeWork
流程的标记,最后完成整个链路: