React源码系列4---commit阶段
render阶段中最后调用的commitRoot是commit阶段的工作起点。fiberRootNode作为传参commitRoot(root)
commit
在,最近的一次更新中react官方使用了rootFiber.firstEffct上保存了一条需要执行副作用的Fiber节点的单向链表effectListsubtreeHasEffects和rootHasEffect获取副作用的Fiber节点
,这些Fiber节点的updateQueue中保存了变化的props- 这些
副作用对应的DOM操作会在commit阶段执行 - 部分生命周期钩子函数
componentDid***,hook(useEffect)需要在commit阶段执行
commit阶段的主要工作(即Renderer的工作流程):
before mutation阶段(执行DOM操作之前)mutation阶段(执行DOM操作)layout阶段(执行DOM操作之后)
commitRootImpl-->commitRootImpl
在before mutation阶段组件之前以及layout阶段之后还涉及到useEffect的触发,优先级重置,ref的绑定/解绑等
commit流程
before mutation 之前
commitRootImpl方法中一直到if (subtreeHasEffects || rootHasEffect)之前,属于before mutation之前
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
// passive effects. So we need to keep flushing in a loop until there are
// no more pending effects.
// TODO: Might be better if `flushPassiveEffects` did not automatically
// flush synchronous work at the end, to avoid factoring hazards like this.
// 触发useEffect回调与其他的同步任务,由于可能会引发新的渲染。所以这里需要遍历到最终没有任务为止
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// root : fiberRootNode
// root.finishedLanes 指当前应用的rootFiber
const finishedWork = root.finishedWork;
// lanes 与优先级相关的变量
const lanes = root.finishedLanes;
if (enableSchedulingProfiler) {
markCommitStarted(lanes);
}
if (finishedWork === null) {
if (enableSchedulingProfiler) {
markCommitStopped();
}
return null;
}
// 重置Scheduler绑定的回调
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 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;
// Update the first and last pending times on this root. The new first
// pending time is whatever is left on the root fiber.
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 重置优先级变量
markRootFinished(root, remainingLanes);
// Clear already finished discrete updates in case that a later call of
// `flushDiscreteUpdates` starts a useless render pass which may cancels
// a scheduled timeout.
// 清理已经完成的discrete updates,eg: 鼠标点击触发的更新
if (rootsWithPendingDiscreteUpdates !== null) {
if (
!hasDiscreteLanes(remainingLanes) &&
rootsWithPendingDiscreteUpdates.has(root)
) {
rootsWithPendingDiscreteUpdates.delete(root);
}
}
// 重置全局变量
if (root === workInProgressRoot) {
// We can reset these now that they are finished.
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
// This indicates that the last root we worked on is not the same one that
// we're committing now. This most commonly happens when a suspended root
// times out.
}
// Check if there are any effects in the whole tree.
// TODO: This is left over from the effect list implementation, where we had
// to check for the existence of `firstEffect` to satsify Flow. I think the
// only other reason this optimization exists is because it affects profiling.
// Reconsider whether this is necessary.
// 判断当前的树上面是否还有副作用的节点
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
} else {
// No effects.
root.current = finishedWork;
// Measure these anyway so the flamegraph explicitly shows that there were
// no effects.
// TODO: Maybe there's a better way to report this.
if (enableProfilerTimer) {
recordCommitTime();
}
}
before mutation之前的阶段都是在处理变量的赋值,以及状态的重置等操作
layout 之后
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// useEffect相关
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {}
// 性能优化相关
if (remainingLanes !== NoLanes) {
if (enableSchedulerTracing) {
// ...
}
} else {
// ...
}
// 性能优化相关
if (enableSchedulerTracing) {
if (!rootDidHavePassiveEffects) {
// ...
}
}
// ...检测无限循环的同步任务
if (remainingLanes === SyncLane) {
// ...
}
// 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
ensureRootIsScheduled(root, now());
// ...处理未捕获错误及老版本遗留的边界问题
// 执行同步任务,这样同步任务不需要等到下次事件循环再执行
// 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
// 或useLayoutEffect
flushSyncCallbackQueue();
return null;
主要包含三个方面:
useEffect的相关处理- 性能追踪
- 在
commit阶段会触发一些生命周期钩子(componentDid***)和hook(useLayoutEffect,useEffect)
在这些回调方法中可能会触发新的更新,新的更新会开启新的
render-commit过程,例如:
- 触发点击事件,页面中令一个状态变为
0- 在
useLayoutEffect中设置为随机数- 页面上渲染的数字不是
0,而是变成随机数
因为useEffectLayout在layout阶段同步执行回调,回调中触发状态更新,这个操作会重新调度一个新的任务:--->flushSyncCallbackQueue
// If layout work was scheduled, flush it now.
flushSyncCallbackQueue();
所以看不到页面中的数字先变成0
before mutation
Renderer工作的阶段被称为commit阶段,commit阶段分为三个部分
-
before mutation(执行
DOM操作前) -
mutation阶段(执行
DOM操作) -
layout阶段(执行
DOM操作之后)// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);
// 将当前上下文标记为CommitContext,作为commit阶段的标志
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 处理focus状态
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
// beforeMutation阶段的主函数
commitBeforeMutationEffects(finishedWork);
focusedInstanceHandle = null;
这里需要关注主函数`commitBeforeMutationEffects`的作用
### commitBeforeMutationEffects
`commitBeforeMutationEffects` 直接去调度`commitBeforeMutationEffectsImpl`
代码逻辑:
``` javascript
function commitBeforeMutationEffects(firstChild: Fiber) {
let fiber = firstChild;
while (fiber !== null) {
if (fiber.deletions !== null) {
commitBeforeMutationEffectsDeletions(fiber.deletions);
}
if (fiber.child !== null) {
const primarySubtreeFlags = fiber.subtreeFlags & BeforeMutationMask;
if (primarySubtreeFlags !== NoFlags) {
commitBeforeMutationEffects(fiber.child);
}
}
try {
commitBeforeMutationEffectsImpl(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
fiber = fiber.sibling;
}
}
commitBeforeMutationEffectsImpl:
function commitBeforeMutationEffectsImpl(fiber: Fiber) {
const current = fiber.alternate;
const flags = fiber.flags;
// focus相关
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// Check to see if the focused element was inside of a hidden (Suspense) subtree.
// TODO: Move this out of the hot path using a dedicated effect tag.
if (
fiber.tag === SuspenseComponent &&
isSuspenseBoundaryBeingHidden(current, fiber) &&
doesFiberContain(fiber, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur();
}
}
// 调用getSnapshotBeforeUpdate
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(fiber);
commitBeforeMutationEffectOnFiber(current, fiber);
resetCurrentDebugFiberInDEV();
}
// 调度useEffect
if ((flags & Passive) !== NoFlags) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
}
整体分为三个部分:
- 处理
DOM节点渲染/删除后的autoFocus,blur逻辑 - 调用
getSnapshotBeforeUpdate生命周期钩子 - 调度
useEffect
调用getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名,
这个方法内部会调用getSnapshotBeforeUpdate:
commitBeforeMutationLifeCycles方法
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
getSnapshotBeforeUpdate在commit阶段内的before mutation阶段调用的,由于commit是同步进行的,所以不会出现多次调用的问题
调度useEffect
scheduleCallback方法就是由Scheduler模块提供,用于某个优先级异步调度一个回调函数
// 调度useEffect
if ((flags & Passive) !== NoFlags) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
此处,被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects
如何异步调度
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
const priorityLevel =
pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
? NormalSchedulerPriority
: pendingPassiveEffectsRenderPriority;
pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
if (decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
setCurrentUpdateLanePriority(
schedulerPriorityToLanePriority(priorityLevel),
);
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
}
} else {
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
}
}
return false;
}
通过之前的了解render阶段:Fiber节点更新过程中的副作用包括
- 插入
DOM节点(Placement) - 删除
DOM节点(Deletion) - 更新
DOM节点(Update)
除此之外,当一个FunctionComponent含有useEffect或者useLayoutEffect的时候,他对应的Fiber节点也会被赋值flags
在commitRootImpl方法中会循环遍历rootWithPendingPassiveEffects执行有副作用的回调函数
整个useEffect的异步调用分为三个部分:
before mutation阶段在scheduleCallback中调度flushPassiveEffectslayout阶段之后将effectList赋值给rootWithPendingPassiveEffectsscheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects
useEffect: 在浏览器完成布局与绘制之后,传递给useEffect的函数会延迟调用
使用useEffect异步执行的原因主要是为了纺织同步执行时阻塞浏览器的渲染
总结
before mutation阶段会通过commitBeforeMutationEffects去遍历有effect的Fiber节点:
- 处理DOM节点渲染/删除后的
Focus/onblur逻辑 - 调用
getSnapshotBeforeUpdate生命周期 - 调度
useEffect
mutation阶段
与before mutation阶段相似的是mutation阶段也会去遍历effect的Fiber节点,不同的是:mutation阶段调用的是commitBeforeMutationEffects方法
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(finishedWork, root, renderPriorityLevel);
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
}
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;
总结
mutation阶段会遍历有effect的Fiber节点,主要方法在commitMutationEffects,根据flags状态的不同调用不同的处理方法去处理Fiber
layout阶段
- 在当前阶段代码都在
DOM渲染完成(mutation阶段)之后执行的 - 当前阶段触发的生命周期钩子和
hook可以直接访问到已经改变之后的DOM,当前阶段可以直接参与DOM layout的阶段