概述
在render阶段的末尾会调用commitRoot(root),进入commit阶段,这里的root指的就是fiberRoot。然后会遍历render阶段生成的effectList,effectList上的Fiber节点保存着对应的props变化。之后会遍历effectList进行对应的dom操作和生命周期、hooks回调或销毁函数等。
commit阶段的主要工作(即renderer的工作流程)分为三部分:
- before mutation阶段(执行DOM操作前)
- mutation阶段(执行DOM操作)
- layout阶段(执行DOM操作后)
在before mutation阶段之前和layout阶段之后还有些额外的工作,比如:useEffect的触发、优先级相关设置、ref解绑。下面是整个commit阶段的流程图,结合代码和图一起看会很清楚。
commitRoot函数中其实是调用了commitRootImpl函数。
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
在commitRootImpl的函数中主要分三个部分:
- commitBeforeMutationEffects,commit阶段的前置工作
- commitMutationEffects,mutation阶段
- commitLayoutEffects,layout阶段(也就是mutation后)
mutation前置阶段
这个阶段主要做的事:
- 调用flushPassiveEffects执行完所有effect的任务
- 初始化相关变量
- 赋值firstEffect给后面遍历effectList用
do {
// 调用flushPassiveEffects执行完所有effect的任务
// 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// root指 fiberRootNode
// root.finishedWork指当前应用的rootFiber
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// 重置变量 finishedWork指rootFiber
root.finishedWork = null;
// 重置优先级
root.finishedLanes = NoLanes;
// 重置Scheduler绑定的回调函数
root.callbackNode = null;
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 重置优先级相关变量
markRootFinished(root, remainingLanes);
// 清除已完成的discrete updates,例如:用户鼠标点击触发的更新
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 {
}
// 将effectList赋值给firstEffect
// 由于每个fiber的effectList只包含他的子孙节点
// 所以根节点如果有effectTag则不会被包含进来
// 所以这里将有effectTag的根节点插入到effectList尾部
// 这样才能保证有effect的fiber都在effectList中
// rootFiber可能会有新的副作用 将它也加入到effectList
let firstEffect;
if (finishedWork.flags > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}
可以看到,before mutation之前主要做一些变量赋值,状态重置的工作。
mutation的三个函数
这个阶段做的主要事:
- 遍历effectList
- 分别执行三个方法commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects执行对应的dom操作和生命周期
我们知道react有双缓存,我们在构建完workInProgress Fiber树之后会将fiberRoot的current指向workInProgress Fiber,让workInProgress Fiber成为current,这个步骤发生在commitMutationEffects函数和commitLayoutEffects之间。
function commitRootImpl(root, renderPriorityLevel) {
// ....
// 以上的代码属于before mutation
if (firstEffect !== null) {
//...
do {
//...
commitBeforeMutationEffects();
} while (nextEffect !== null);
do {
//...
commitMutationEffects(root, renderPriorityLevel); //commitMutationEffects
} while (nextEffect !== null);
root.current = finishedWork; //切换current Fiber树
do {
//...
commitLayoutEffects(root, lanes);//commitLayoutEffects
} while (nextEffect !== null);
//...
}
}
- componentWillUnmount在commitMutationEffects函数中执行,这时还可以获取之前的Update
- componentDidMount和componentDidUpdate在commitLayoutEffects函数中执行,这个时候可以获取更新后的dom了。
commitBeforeMutationEffects
该函数属于mutation阶段前置阶段,主要做的事:
-
处理DOM节点渲染/删除后的 autoFocus、blur逻辑。
function commitBeforeMutationEffects() { while (nextEffect !== null) { const current = nextEffect.alternate; if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { // ...focus blur相关 if ((nextEffect.flags & Deletion) !== NoFlags) { if (doesFiberContain(nextEffect, focusedInstanceHandle)) { shouldFireAfterActiveInstanceBlur = true; beforeActiveInstanceBlur(); } } else { if ( nextEffect.tag === SuspenseComponent && isSuspenseBoundaryBeingHidden(current, nextEffect) && doesFiberContain(nextEffect, focusedInstanceHandle) ) { shouldFireAfterActiveInstanceBlur = true; beforeActiveInstanceBlur(); } } } } -
调用
getSnapshotBeforeUpdate生命周期钩子。function commitBeforeMutationEffects() { while (nextEffect !== null) { // ... const flags = nextEffect.flags; // 调用getSnapshotBeforeUpdate if ((flags & Snapshot) !== NoFlags) { setCurrentDebugFiberInDEV(nextEffect); commitBeforeMutationEffectOnFiber(current, nextEffect); resetCurrentDebugFiberInDEV(); } }commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycle的别名。
在该方法内会调用getSnapshotBeforeUpdate
值得一提的是,从react16开始,
componentWillXXX的钩子前面增加了UNSAFE_的前缀,究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。为此,
getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以getSnapshotBeforeUpdate也是同步的,不会遇到多次调用的问题。 -
调度useEffect。
scheduleCallback方法由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。function commitBeforeMutationEffects() { while (nextEffect !== null) { // ... // 调度useEffect if ((flags & Passive) !== NoFlags) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; scheduleCallback(NormalSchedulerPriority, () => { // 触发useEffect flushPassiveEffects(); return null; }); } } nextEffect = nextEffect.nextEffect; //遍历effectList } }被异步调度的回调函数就是触发
useEffect的方法flushPassiveEffects,那为什么是异步调度的呢?react文档,effect的执行时机:
与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
可见,
useEffect异步执行的原因主要是防止同步执行时阻塞浏览器渲染
commitMutationEffects
这个函数主要做的事:
- 根据ContentReset effectTag重置文字节点
- 更新ref
- 根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating)
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍历effectList
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 根据 ContentReset effectTag重置文字节点
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 更新ref
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 根据 effectTag 分别处理
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
// 插入DOM 并 更新DOM
case PlacementAndUpdate: {
// 插入
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 更新
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// SSR
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
// SSR
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新DOM
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除DOM
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
根据不同的Tag执行不同的操作:
-
Placement effect
当Fiber节点含有Placement effectTag,说明该Fiber节点对应的DOM节点需要插入到页面中。
调用的方法为commitPlacement。
// 当Fiber节点含有Placement effectTag,意味着该Fiber节点对应的DOM节点需要插入到页面中。调用的方法为commitPlacement function commitPlacement(finishedWork: Fiber): void { if (!supportsMutation) { return; } // 获取父级DOM节点。其中finishedWork为传入的Fiber节点 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: .... case HostRoot: ... case HostPortal: ... case FundamentalComponent: ... default: } if (parentFiber.flags & ContentReset) { // Reset the text content of the parent before doing any insertions resetTextContent(parent); // Clear ContentReset from the effect tag parentFiber.flags &= ~ContentReset; } // 获取Fiber节点的DOM兄弟节点 const before = getHostSibling(finishedWork); // 根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作 if (isContainer) { insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); } else { insertOrAppendPlacementNode(finishedWork, before, parent); } } -
Update effect
当Fiber节点含有Update effectTag,说明该Fiber节点需要更新。调用的方法为
commitWork,他会根据Fiber.tag分别处理。function commitWork(current: Fiber | null, finishedWork: Fiber): void { if (!supportsMutation) { switch (finishedWork.tag) { //... case SimpleMemoComponent: { commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); } //... } } switch (finishedWork.tag) { //... case HostComponent: { //... commitUpdate( instance, updatePayload, type, oldProps, newProps, finishedWork, ); } return; } } -
Deletion effect
当Fiber节点含有Deletion effectTag,说明该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为
commitDeletion该方法会执行如下操作:
- 递归调用Fiber节点及其子孙Fiber节中fiber.tag为ClassComponent的componentWillUnmount生命周期钩子,从页面移除Fiber节对应DOM节点
- 解绑ref
- 调度useEffect的销毁函数
function commitDeletion( finishedRoot: FiberRoot, current: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { if (supportsMutation) { unmountHostComponents(finishedRoot, current, renderPriorityLevel); } else { commitNestedUnmounts(finishedRoot, current, renderPriorityLevel); } const alternate = current.alternate; detachFiberMutation(current); if (alternate !== null) { detachFiberMutation(alternate); } }
commitLayoutEffects
该函数属于layout阶段,主要做的事:
- 调用commitLayoutEffectOnFiber执行相关生命周期函数或者hook相关callback
- 执行commitAttachRef为ref赋值
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 调用commitLayoutEffectOnFiber执行生命周期和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// ref赋值
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理:
- 对于
ClassComponent,他会通过current === null?区分是mount还是update,调componentDidMount或componentDidUpdate。 - 对于
FunctionComponent及相关类型,他会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数
在源码中commitLayoutEffectOnFiber函数的别名是commitLifeCycles,在简化后的代码中可以看到,commitLifeCycles会判断fiber的类型,SimpleMemoComponent会执行useLayoutEffect的回调,然后调度useEffect,ClassComponent会执行componentDidMount或者componentDidUpdate,this.setState第二个参数也会执行,HostRoot会执行ReactDOM.render函数的第三个参数,例如
ReactDOM.render(<App />, document.querySelector("#root"), function() {
console.log("root mount");
});
现在可以知道useLayoutEffect是在commit阶段同步执行,useEffect会在commit阶段异步调度
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case SimpleMemoComponent: {
// 此函数会调用useLayoutEffect的回调
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 向pendingPassiveHookEffectsUnmount和pendingPassiveHookEffectsMount中push effect // 并且调度它们
schedulePassiveEffects(finishedWork);
}
case ClassComponent: {
//条件判断...
instance.componentDidMount();
//条件判断...
instance.componentDidUpdate(//update 在layout期间同步执行
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
case HostRoot: {
commitUpdateQueue(finishedWork, updateQueue, instance);//render第三个参数
}
}
}
在schedulePassiveEffects中会将useEffect的销毁和回调函数push到pendingPassiveHookEffectsUnmount和pendingPassiveHookEffectsMount中
function schedulePassiveEffects(finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {next, tag} = effect;
if (
(tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
//push useEffect的销毁函数并且加入调度
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
//push useEffect的回调函数并且加入调度
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
commitAttachRef:
commitAttachRef中会判断ref的类型,执行ref或者给ref.current赋值
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
if (typeof ref === "function") {
// 执行ref回调
ref(instanceToUse);
} else {
// 如果是值的类型则赋值给ref.current
ref.current = instanceToUse;
}
}
}
layout阶段
这个阶段主要做的事:
- 根据rootDoesHavePassiveEffects赋值相关变量
- 执行flushSyncCallbackQueue处理componentDidMount等生命周期或者useLayoutEffect等同步任务
// layout阶段
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// 根据rootDoesHavePassiveEffects赋值相关变量
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {}
//...
// 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
ensureRootIsScheduled(root, now());
// ...
// 执行同步任务,这样同步任务不需要等到下次事件循环再执行
// 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
// 或useLayoutEffect
flushSyncCallbackQueue();
return null;
current Fiber树切换
在结束本节的学习前,我们看下这行代码:
root.current = finishedWork;
workInProgress Fiber树在commit阶段完成渲染后会变为current Fiber树。这行代码的作用就是切换fiberRootNode指向的current Fiber树。