对于commit阶段的主要工作是循环effectList链表去将有更新的fiber节点应用到页面上是commit的主要工作。
EffectList
什么是副作用?
函数在执行过程中对外部造成的影响可以称之为副作用,副作用包含的类型很多,比如说标记值为Placement时,会叫Dom节点进行插入与移动,Passive代表useEffect进行回调执行,ChildDeletion指移除子Dom节点
对于React的工作原理可以简单概括为
- 触发更新
- render阶段:计算更新会造成的副作用
- commit阶段:执行副作用
更新Dom变化主要就是Placement、ChildDeletion在起作用,在render阶段来进行保存副作用,在commit阶段来使用副作用。
什么是EffectLitst
在重构前,render阶段带有副作用的节点会链接形成链表,这条链表被称为EffectList,在commit阶段,不需要深度优先遍历整棵树,只需要遍历Effects List就能找到所有有副作用的节点并执行对应操作。
在render阶段的complete阶段,如果遇到flags字段不为NoFlags时(代表需要副作用),此时就把该Fiber节点添加到EffectList中。
例一:如下图,会通过EffectList来将副作用节点进行链接,避免了深度优先遍历对应的Fiber树。
例二:
<div id="1">
<div id="4"/>
<div id="2">
<div id="3"/>
</div>
</div>
最终形成的EffectList为:
firstEffect => div4
div4.nextEffect => div3
div3.nextEffect => div2
div2.nextEffect => div1
// div4为firstEffect,div1为lastEffect
因为Fiber树是通过深度优先遍历来进行构建,所以div4先完成completeWork来构建出firsteEffect
commit阶段
React16
引入 Fiber
架构后,将整个调度分为了两个阶段 render & commit
,在 render
阶段,React
会计算 DOM
的更新,并将所有需要更新的 fiber
整理成一个 effect list
,在 commit
阶段中, React
会遍历 effect list
执行所有的副作用,期间会执行更新相关的生命周期、挂载 DOM 等等。
在commit阶段的入口是commitRoot函数,它会告知scheduler以立即执行的优先级去调度commit阶段的工作。
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
scheduler去调度的是commitRootImpl
,它是commit阶段的核心实现,整个commit阶段被划分成三个部分。
commitRootImpl
对于React中会有一个节点名为FiberRoot来作为整个组件树的顶层节点,对于finishedWork属性指向的是完成的第一个work,也为ReactDom.render的组件,在commit阶段中我们通过根节点拿到finishedWork,然后通过finishedWork来获取firstEffect
// 如果 root 有副作用的话,其副作用将会放置在 effectList 的末尾,
// root 无副作用的话,那么 firstEffect 就是根组件的 firstEffect
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;
}
对于commit阶段,React将其拆分为了多个小阶段,每个小阶段都会执行对于nextEffect对应的遍历流程,保证了useEffect可以在useLayoutEffect之前被处理,对应代码如下
// ReactFiberWorkLoop.js
function commitRootImpl() {
// 刷新所有的 PassiveEffect
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// Get the list of effects.
// effectList 的第一个节点
let firstEffect;
// ...
// 省略 if 判断,如果 root 有副作用的话,其副作用将会放置在 effectList 的末尾,root 无副作用的话,那么 firstEffect 就是根组件的 firstEffect
firstEffect = finishedWork.firstEffect;
if (firseEffect !== null) {
nextEffect = firstEffect;
// 每一阶段的详细代码后续会进行说明
// 第一阶段,before mutation
do {
commitBeforeMutationEffects();
} while(nextEffect !== null)
// ...
// 将游标重置,指向 effect list 头
nextEffect = firstEffect;
// 第二阶段 mutation
do {
commitMutationEffects(root, renderPriorityLevel);
} while(nextEffect !== null)
// 将当前的 workInProgress树 作为 current 树
root.current = finishedWork;
// ...
// 第三阶段 layout
do {
commitLayoutEffects(root, expirationTime);
} while(nextEffect)
// 让调度器在 帧 的末尾暂停,给浏览器机会执行一次 重绘
requestPaint();
// rootDoesHavePassiveEffects 标志位判断,该标志位是在 commit 第一阶段进行设置,标记当前 commit 是否具有 passiveEffect
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsExpirationTime = expirationTime;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {
// 遍历 effect list 逐个设置为 null 以便 GC
}
// 确保 root 上所有的 work 都被调度完
ensureRootIsScheduled(root);
// 检测在 useLayoutEffect 中是否做了布局修改等,刷新布局.
// 如果在 layoutEffect 中调用了 setState 也会在该函数中检测中并开启新的一轮调度
flushSyncCallbackQueue();
} else { ... }
}
Before Mutation
对于before mutation针对类组件调用getSnapshotBeforeUpdate,让我们可以在Dom变更前获取组件实例的信息。对于函数式组件会进行异步调度useEffect。
对于Before Mutation阶段的入口函数是commitBeforeMutationEffects,其函数源码如下:
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 只有class组件会进入这个判断
// 对于使用 getSnapShowBeforeUpdate 的组件 fiber.effectTag |= SnapShot
if ((effectTag & Snapshot) !== NoEffect) {
// ...
const current = nextEffect.alternate;
// 执行 getSnapShotBeforeUpdate 生命周期
commitBeforeMutationEffectOnFiber(current, nextEffect);
resetCurrentDebugFiberInDEV();
}
// 对于使用 useEffect 的组件,其 Fiber.effectTag = UpdateEffect | PassiveEffect
if ((effectTag & Passive) !== NoEffect) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalPriority, () => {
// 执行useEffect
flushPassiveEffects();
return null;
});
}
}
// 向下遍历
nextEffect = nextEffect.nextEffect;
}
}
可以将上述源码分为两个部分。
第一个主要用于判断Class组件中的getSnapShotBeforeUpdate,进入第一个判断时,会在内部执行getSnapShotBeforeUpdate生命周期,也只有Class组件会进入当前判断。
第二个判断主要用于函数式组件且使用了useEffect。
- 对rootDoesHavePassiveEffects进行标记,在commit阶段的末尾会判断当前commit是否具有被动的副作用,如果具有被动的副作用,则会设置一些额外的标志位用于下一轮的调度。
- 然后再进行加入回调,在回调内部会进行调用flushPassiveEffects。
对于回调会被异步触发,这里的异步不是setState那样的批处理,而是事件循环中的同步任务与异步任务。当创建scheduleCallback时,此时就会创建一个task任务,当前task保存着我们传入的回调函数,将其压入到taskQueue队列中,当浏览器的每一帧还有剩余时间时,React会调用postMessage将performWorkUntilDeadline函数压入到异步队列中,等待所有的同步任务执行完之后执行该函数。源码如下:
// Scheduler.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
// ...
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
// ...
if (startTime > currentTime) {
//... 超时调用
} else {
// 正常调用
push(taskQueue, newTask);
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// 发送一个 postMessage
requestHostCallback(flushWork);
}
}
}
// ScheduleHostConfig.default.js
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
对于flushPassiveEffects主要用于刷新useEffect,并且与scheduleCallback这个回调函数结合,会被添加进异步任务队列中等待执行。
Mutation
在Mutation阶段后会将workInProgress Tree变为current树,在mutation阶段主要执行的函数为commitMutationEffects,对于commitMutationEffects函数的代码如下:
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffct) {
// 获取当前的effectTag来匹配对应的副作用标签
const effectTag = nextEffect.effectTag;
let primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
// ...
switch(primaryEffectTag) {
// 单纯的挂载 DOM
case Placement: {
// 挂载 DOM
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
return;
}
// 更新组件及DOM
case PlacementAndUpdate: {
// 挂载 DOM
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 刷新 layoutEffect.desotry
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;
}
// ...
}
nextEffect = nextEffect.nextEffect;
}
}
对于effectTag与对应匹配的含义如下:
- Placement:当增加Dom时,会加入对应的标志位
- PlacementAndUpdate:当Dom变化,并组件发生更新时
- Update:组件更新,但Dom无变化时
- Deletion:当组件卸载时,会加入对应的标志位
不同的标志位,其内部调用的逻辑比较相似,主要的函数为commitPlacement和commitWork。
对于commitPlacement函数源码分析如下:
function commitPlacement(finishedWork: Fiber): void {
...
// 找到目标节点DOM层面的父节点(parent)
const parentFiber = getHostParentFiber(finishedWork);
// 根据目标节点类型,改变parent
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
// 对于HostRoot和HostPortal都没有对应的Dom节点
switch (parentFiber.tag) {
// 原生的HTML标签
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;
}
}
if (parentFiber.effectTag & ContentReset) {
// 插入之前重设文字内容
resetTextContent(parent);
// 删除ContentReset的effectTag
parentFiber.effectTag &= ~ContentReset;
}
// 找到基准节点
const before = getHostSibling(finishedWork);
// 根据上方isContainer的标识执行不同的插入操作
// 执行插入操作
if (isContainer) {
// 在外部DOM节点上插入
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
// 直接在父节点插入
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
对于commitPlacement函数的主要功能步骤如下:
- 找到目标节点的父节点
- 根据目标节点类型,找到对应的赋值对应的parent
- 如果目标节点对应的DOM节点目前只有文字内容,类似
<div>hello</div>
,并且持有ContentReset(内容重置)的effectTag,那么插入节点之前先设置一下文字内容 - 找到基准节点
- 执行插入
对于CommitWork的源码如下:
对于commitWork主要讲解Dom节点(HostComponent)的更新和文本节点(HostText)的更新
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
switch (finishedWork.tag) {
// 更新HostComponent是更新fiber节点的props
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// updateQueue已经是在complete过程中节点props被diff的结果
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
// 更新对应Dom节点的props
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
// 更新对应Dom节点的事件监听器
if (enableFlareAPI) {
const prevListeners = oldProps.listeners;
const nextListeners = newProps.listeners;
if (prevListeners !== nextListeners) {
updateEventListeners(nextListeners, finishedWork, null);
}
}
}
return;
}
case HostText: {
const textInstance: TextInstance = finishedWork.stateNode;
// 通过props来获取对应的文本
const newText: string = finishedWork.memoizedProps;
const oldText: string =
current !== null ? current.memoizedProps : newText;
// 最后通过commitTextUpdate来更新文本
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
}
}
Layout
对于layout阶段的入口函数是commitLayoutEffects,对于classComponent和functionComponent,针对前者,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对后者,填充useEffect 的 effect执行数组。
commitLayoutEffects源码如下:
function commitLayoutEffects(
root: FiberRoot,
committedExpirationTime: ExpirationTime,
) {
while (nextEffect !== null) {
//...
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
// 执行componentDidmount
commitLayoutEffectOnFiber(
root,
current,
nextEffect,
committedExpirationTime,
);
}
// ...
nextEffect = nextEffect.nextEffect;
}
}
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的创建
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 填充useEffect的effect执行数组
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
// 如果是初始挂载阶段,调用componentDidMount
instance.componentDidMount();
} else {
// 如果是更新阶段,调用componentDidUpdate
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
instance.componentDidUpdate(
prevProps,
prevState,
// 将getSnapshotBeforeUpdate的结果传入
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
// 调用setState的回调
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
...
}
}
commit最后
在上述三个子阶段都完成之后, React
已经将所有的DOM都挂载到屏幕上,并且也执行了 didMount
,但是在上述三个子阶段的执行过程中,可能会发生新的副作用,比如说在 layoutEffect
中调用了 setState
,因此在 commit
的末尾会做一次检测,入口函数就是 flushSyncCallbackQueue
。
总结
对于commit阶段分为三个过程,分别为before Mutation/mutation/layout,将effectList的处理分为三个阶段保证可以在不同生命周期函数中进行调用,相对于同步执行的useEffectLayout,对于useEffect的异步调用提供了一种不阻塞页面渲染的副作用操作入口。最后标记还未处理的优先级和调用ensureRootIsScheduled函数来确保root上所有被跳过的低优先级任务再次被调度。
在before mutation这个阶段,对于class组件而言,执行getSnapShotBeforeUpdate对应的生命周期,对于函数式组件会进行异步回调执行flushPassiveEffects,等待同步任务执行完毕后执行useEffects对应的副作用。
在mutation阶段会挂载和更新Dom,并清理上一轮的useLayoutEffect。此时会将当前的workInProgress树作为 current树。
在layout阶段中对于Class组件会执行componentDidMount生命周期,对于函数式组件会执行useLayoutEffect。