React源码解析————Commit阶段(一)
2021SC@SDUSC
2021SC@SDUSC
commit阶段
前几篇我们介绍了render阶段,现在我们来看commit阶段,commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参。
同时在rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的单向链表effectList,这些Fiber节点的updateQueue中保存了变化的props。
这些副作用对应的DOM操作在commit阶段执行。
除此之外,一些生命周期钩子(比如componentDidXXX)、hook(比如useEffect)也需要在commit阶段执行。
commit阶段的主要工作(即Renderer的工作流程)可以分为三部分:
1.before mutation阶段(执行DOM操作前)
2.mutation阶段(执行DOM操作)
3.layout阶段(执行DOM操作后)
commitRoot
commit阶段的入口是commitRoot函数,它会告知scheduler以立即执行的优先级去调度commit阶段的工作。
function commitRoot(root) {
// TODO: This no longer makes any sense. We already wrap the mutation and
// layout phases. Should be able to remove.
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(root, previousUpdateLanePriority);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
可以看到我们做的大部分工作其实都是在commitRootImpl中完成的。
注:其中代码中已经删去部分注释和内容,也加上了自己的理解,如果想查看完整版请自行去GitHub上下载react-main,谢谢
function commitRootImpl(root, renderPriorityLevel) {
// 进入commit阶段,先执行一次之前未执行的useEffect
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// 准备阶段-----------------------------------------------
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackId = NoLanes;
// effectList的整理,将root上的effect连到effectList的末尾
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
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;
}
// 准备阶段结束,开始处理effectList
if (firstEffect !== null) {
...
// before mutation阶段--------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
...
// mutation阶段---------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
// 将wprkInProgress树切换为current树
root.current = finishedWork;
// layout阶段-----------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
nextEffect = null;
// 通知浏览器去绘制
requestPaint();
} else {
// 没有effectList,直接将wprkInProgress树切换为current树
root.current = finishedWork;
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// 获取尚未处理的优先级,比如之前被跳过的任务的优先级
remainingLanes = root.pendingLanes;
// 将被跳过的优先级放到root上的pendingLanes(待处理的优先级)上
markRootFinished(root, remainingLanes);
/*
* 每次commit阶段完成后,再执行一遍ensureRootIsScheduled,确保是否还有任务需要被调度。
* 例如,高优先级插队的更新完成后,commit完成后,还会再执行一遍,保证之前跳过的低优先级任务
* 重新调度
*
* */
ensureRootIsScheduled(root, now());
...
return null;
}
下面我们对这上述三个阶段分别进行的详细讲解。
before mutation阶段
before mutation阶段的代码在commitRootlmpl中其实很短,整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 0;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 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.
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
先保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
再将当前上下文标记为CommitContext,作为commit阶段的标志
我们重点关注beforeMutation阶段的主函数commitBeforeMutationEffects做了什么。
commitBeforeMutationEffects
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
// We no longer need to track the active instance fiber
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
我们可以看到代码主要是调用了commitBeforeMutationEffects_begin函数
代码如下:
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// This phase is only used for beforeActiveInstanceBlur.
// Let's skip the whole loop if it's off.
if (enableCreateEventHandleAPI) {
// TODO: Should wrap this in flags check, too, as optimization
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const deletion = deletions[i];
commitBeforeMutationEffectsDeletion(deletion);
}
}
}
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
ensureCorrectReturnPointer(child, fiber);
nextEffect = child;
} else {
commitBeforeMutationEffects_complete();
}
}
}
它的作用主要是调用commitBeforeMutationEffects_complete函数,然后在commitBeforeMutationEffects_complete中尝试调用commitBeforeMutationEffectsOnFiber(fiber);
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
setCurrentDebugFiberInDEV(fiber);
try {
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
reportUncaughtErrorInDEV(error);
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名。在该方法内会调用类组件的getSnapshotBeforeUpdate,针对函数组件,异步调度useEffect。
注:代码已删去部分,只保留了switch部分。
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
break;
}
case ClassComponent: {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (__DEV__) {
if (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'getSnapshotBeforeUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentNameFromFiber(finishedWork) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'getSnapshotBeforeUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentNameFromFiber(finishedWork) || 'instance',
);
}
}
}
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
if (__DEV__) {
const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set<mixed>);
if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
didWarnSet.add(finishedWork.type);
console.error(
'%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' +
'must be returned. You have returned undefined.',
getComponentNameFromFiber(finishedWork),
);
}
}
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
case HostRoot: {
if (supportsMutation) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
break;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
break;
default: {
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
}
resetCurrentDebugFiberInDEV();
}
}
我们可以看见,getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题。
调度useEffect
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
在这几行代码内,scheduleCallback方法由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。
在此处,被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects。
我们接下来讨论useEffect如何被异步调度:
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
// TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should
// probably just combine the two functions. I believe they were only separate
// in the first place because we used to wrap it with
// `Scheduler.runWithPriority`, which accepts a function. But now we track the
// priority within React itself, so we can mutate the variable directly.
if (rootWithPendingPassiveEffects !== null) {
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
const prevTransition = ReactCurrentBatchConfig.transition;
const previousPriority = getCurrentUpdatePriority();
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(priority);
return flushPassiveEffectsImpl();
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
return false;
}
在flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList。
我们知道,effectList中保存了需要执行副作用的Fiber节点。其中副作用包括
1.插入DOM节点(Placement)
2.更新DOM节点(Update)
3.删除DOM节点(Deletion)
除此外,当一个FunctionComponent含有useEffect或useLayoutEffect,他对应的Fiber节点也会被赋值effectTag。
在flushPassiveEffects方法内部会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。
如果在此时直接执行,rootWithPendingPassiveEffects === null。
那么rootWithPendingPassiveEffects会在何时赋值呢?
这里我们会根据rootDoesHavePassiveEffects === true?决定是否赋值rootWithPendingPassiveEffects。
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
// This commit has passive effects. Stash a reference to them. But don't
// schedule a callback until after flushing layout work.
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
}
所以整个useEffect异步调用分为三步:
1.before mutation阶段在scheduleCallback中调度flushPassiveEffects
2.layout阶段之后将effectList赋值给rootWithPendingPassiveEffects
3.scheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects。
下一篇我们会继续讲解mutation阶段和layout阶段。