前面文章所描述的都发生在render过程中。React包含两个过程,即render和commit过程,其中render过程是可以打断的,而commit阶段是不可打断的。
commit阶段可以理解是真正的操作DOM的阶段,其消费render阶段打到Fiber节点上的Flag,并且根据Flag对真实DOM元素进行更新,删除,插入等操作。
先来复习一下Flag,我们知道,commit包含两个子阶段,即:
Mutation阶段: 此阶段主要进行DOM的更新操作,包含DOM的增删改,以及Ref的更新阶段。次阶段也包涵Effect的收集。
Passive阶段: 此阶段是在Mutation阶段完成之后,进行一些副作用的处理阶段,一般在浏览器进行完一个循环的渲染之后执行。但是由于Effect是交给scheduler执行的,如果浏览器在完成Mutation阶段之后还有空闲时间,也有可能先执行useEffect。
对于Muation阶段,React主要处理以下Flags
TypeScript
export const Placement = 0b0000001;
export const Update = 0b0000010;
export const ChildDeletion = 0b0000100;
export const Ref = 0b0010000;
export const MutationMask =
Placement | Update | ChildDeletion | Ref
可以看到,Mutation阶段主要处理 dom元素插入 删除 更新 以及Ref的更新,MutationMask就是这四个Flag的合集。
Passive阶段,主要处理PassiveEffect和ChildDeletion
TypeScript
export const PassiveEffect = 0b0001000;
export const PassiveMask = PassiveEffect | ChildDeletion;
因为useEffect不仅仅是在监听值变动时执行,在组件卸载时,也会执行其返回的函数
PassiveMask为这两个Flags的合集
commit准备工作
在render阶段结束之后,此时已经有一棵完整的待更新Fiber树了。
此时需要为commitRoot做一些初始化工作
-
root.finishedWork用来记录已经生成的待Commit的Fiber树,由于render阶段结束之后,workInProgress已经为空了,这个值需要用root.current.alternate获得。
-
root.finishedLane 用来记录当前处理的lane,需要用wipRootRenderLane赋予
在给这两个属性赋值后,需要吧wipRenderLane置为NoLane
最后调用commitRoot函数,开始commit阶段
TypeScript
// performWorkOnRoot
//任务完成 收尾 commit
// 设置root.finishedWork
root.finishedWork = root.current.alternate;
root.finishedLane = lane;
// 设置wipRootRenderLane = NoLane;
wipRootRenderLane = NoLane;
// 进入commit阶段
commitRoot(root);
commitRoot - 开启commit流程
commitRoot函数用来开启commit阶段,其要做的事情为:
-
拿到root.finishedWork & root.finishedLane 并且把这两个值置为空, 并且调用markRootFinished把当前处理的lane从root.pendingLanes去除,表示已经完成本lane的处理。
-
检测finishedWork根节点是否包含PassiveEffect或者其子节点subtreeFlasg是否包含PassiveMask, 如果包含,则开启Passive阶段,并且将Passive阶段处理函数推入scheduler,在Mutation阶段结束之后调度运行。
-
检测finishedWork的flags和subTreeFlags是否包含 (PassiveMask | MutationMask) 因为Mutation阶段除了要处理阶段的插入删除更新Ref,还需要收集Effect。
-
如果包含Flag,说明需要commit 开启commitMuationEffect
-
把root.current赋finishedWork
-
开启Layout阶段,调用commitLayoutEffect 用来挂载Ref
-
重新开启调度流程,更新结束
实现如下:
TypeScript
/** commit阶段 */
export function commitRoot(root: FiberRootNode) {
const finishedWork = root.finishedWork;
if (finishedWork === null) return;
const lane = root.finishedLane;
root.finishedWork = null;
root.finishedLane = NoLane;
// 从root.pendingLanes去掉当前的lane
markRootFinished(root, lane);
/** 设置调度 执行passiveEffect */
/** 真正执行会在commit之后 不影响渲染 */
/** commit阶段会收集effect到root.pendingPassiveEffect */
// 有删除 或者收集到Passive 都运行
if (
(finishedWork.flags & PassiveMask) !== NoFlags ||
(finishedWork.subTreeFlags & PassiveMask) !== NoFlags
) {
// 调度副作用
scheduler.scheduleCallback(
PriorityLevel.NORMAL_PRIORITY,
flushPassiveEffect.bind(null, root.pendingPassiveEffects)
);
}
/** hostRootFiber是否有effect */
const hostRootFiberHasEffect =
(finishedWork.flags & (MutationMask | PassiveMask)) !== NoFlags;
/** hostRootFiber的子树是否有effect */
const subtreeHasEffect =
(finishedWork.subTreeFlags & (MutationMask | PassiveMask)) !== NoFlags;
/** 有Effect才处理 */
if (hostRootFiberHasEffect || subtreeHasEffect) {
commitMutationEffects(finishedWork, root);
}
// commit完成 修改current指向新的树
root.current = finishedWork;
// commitLayout阶段 处理Attach Ref
commitLayoutEffects(finishedWork, root);
// 确保可以继续调度
ensureRootIsScheduled(root);
}
commitMutationEffect - Mutation阶段
commitMutationEffect是高阶函数commitEffect返回的函数,其调用如下:
TypeScript
export const commitMutationEffects = commitEffect(
"mutation",
MutationMask | PassiveMask,
commitMutationEffectsOnFiber
);
其中commitEffect是个高阶函数,需要传入
-
当前commit的阶段,mutation|layout,
-
需要检测的Mask
-
处理函数callback
这个函数的作用就是返回一个处理函数,这个处理函数会深度优先遍历传入的finsihedWork 树,在向下递的阶段,每一次都查看当前节点的子节点是否还有对应需要检测的Mask,如果有就继续往下找,如果没有就停止,开始归的过程,这样能减少不必要的遍历。在归的过程中,调用callback函数完成对Flags的处理。
TypeScript
/** 高阶函数 用来处理Effect */
function commitEffect(
phrase: "mutation" | "layout",
mask: Flags,
callback: CommitCallback
): CommitCallback {
/** 递归,DFS 找到最深的无subflags的节点 下面的不需要commit了 因为没有副作用 */
return (finishedWork, root) => {
// DFS
let nextFinishedWork = finishedWork;
while (nextFinishedWork !== null) {
if (
(nextFinishedWork.subTreeFlags & mask) !== NoFlags &&
nextFinishedWork.child
) {
// 递
nextFinishedWork = nextFinishedWork.child;
} else {
while (nextFinishedWork !== null) {
// 归
callback(nextFinishedWork, root);
if (nextFinishedWork.sibling !== null) {
nextFinishedWork = nextFinishedWork.sibling;
break;
}
nextFinishedWork = nextFinishedWork.return;
}
}
}
};
}
commitMutationEffectOnFIber
这个函数是传递进commitEffect的callback回调,commitEffect在每次归的过程中,都会调用这个函数处理每个节点。
这个函数的作用是,检查当前Fiber节点上的flag值,根据不同的flag,调用不同的Flag处理函数
TypeScript
/** 用来处理 Mutation副作用 [Placement | Update | ChildDeletion // TODO PassiveEffect] */
const commitMutationEffectsOnFiber: CommitCallback = (finishedWork, root) => {
// 处理每个节点的Effect
// 获取节点的flags
const flags = finishedWork.flags;
// 处理placement
if ((flags & Placement) !== NoFlags) {
// 存在Placement
commitPlacement(finishedWork);
// 去掉副作用flag
// 去掉某个flag: 0b0111&(~0b0100) => 0b0111&0b1011=> 0b0011 去掉了 0b0100
finishedWork.flags &= ~Placement;
}
// 处理Update
if ((flags & Update) !== NoFlags) {
commitUpdate(finishedWork);
finishedWork.flags &= ~Update;
}
// 处理ChildDeletion
if ((flags & ChildDeletion) !== NoFlags) {
const deletion = finishedWork.delections;
deletion.forEach((deleteOldFiber) => {
commitDeletion(deleteOldFiber, root);
});
finishedWork.flags &= ~ChildDeletion;
}
// 处理收集passiveEffect
if ((flags & PassiveEffect) !== NoFlags) {
// 存在被动副作用
commitPassiveEffect(finishedWork, root, "update");
}
// 卸载Ref 只有hostComponent需要卸载
if (finishedWork.tag === HostComponent && (flags & Ref) !== NoFlags) {
const current = finishedWork.alternate;
if (current) {
// 需要卸载current的ref 其实本质上current和finishedWork的ref都是一个
saftyDetachRef(current);
}
// 卸载之后由于可能还会加载ref 所以这里的flag不能~Ref
}
};
commitPlacement 插入节点
commitPlacement函数用来把新创建的Fiber节点插入到当前DOM树上,插入节点需要找到要插入的父节点,以及要插入到父节点的哪个位置,即找到兄弟节点。
需要注意的点是:
-
只有HostComponent节点才能作为parent节点被插入
-
只有HostComponent节点或HostText节点才可以作为被插入节点的兄弟
由于函数节点 Fragment节点 Memo节点等仅在Fiber中体现,不存在于真实DOM中, 所以在寻找父节点和兄弟节点时需要注意。
TypeScript
/** 处理Placement */
function commitPlacement(finishedWork: FiberNode) {
/** 获取finishedWork的hostparent 用来挂载finishedWork对应的DOM (finishedWork可能也不是Host 后面有处理) */
const hostParent = getHostParent(finishedWork) as Container;
/** 获取finishedWork的Host sibling节点 */
const hostSibling = getHostSibling(finishedWork) as Element;
// 拿到parent和sibling了,就可以插入dom了
// hostsibling不存在就是append 存在就是插入
if (hostParent !== null) {
insertOrAppendPlacementNodeIntoConatiner(
finishedWork,
hostParent,
hostSibling
);
}
}
getHostparent
这个函数用来找到待插入节点最近的的父HostComponent节点,其实现如下,就是顺着return一直找,指导找到tag为HostComponent或者return为null(即HostRoot节点)
如果找到HostRoot节点,需要返回hostRoot.stateNode.container节点
TypeScript
/** 获取HostParent
* 获取当前节点的HostComponent/HostRoot parent
*/
function getHostParent(fiber: FiberNode): Element {
let node = fiber.return;
while (node !== null) {
if (node.tag === HostComponent) {
// host component 返回其stateNode
return node.stateNode;
}
if (node.tag === HostRoot) {
// hostRoot 其stateNode -> FiberRootNode 需要通过FiberRootNode.container获取
return node.stateNode.container;
}
// 向上找
node = node.return;
}
return null;
}
getHostSibling
这个函数用来找到和当前插入Fiber节点紧邻的兄弟节点,因为需要使用parent.apppendBefore(sibling)完成节点插入,所以需要找到其兄弟给插入位置定位。
其实现思路是,从当前节点先向上找一个存在sibling的父节点,任意类型都可以,如果在寻找的过程中,找到HostComponent HostRoot 或者 null 的时候,还没有sibling节点,说明当前节点没有兄弟节点,直接返回null (注意 HostComponent代表已经找到了其parent节点了,往上找没意义了)
如图:

实现如下,当while循环因为node.sibling不为null退出时,说明已经找到了存在sibling的父节点
TypeScript
while (node.sibling === null) {
const parent = node.return;
if (
parent === null ||
parent.tag === HostComponent ||
parent.tag === HostRoot
) {
/** 回溯的过程中 如果遇到了 hostComponent / hostRoot 说明找到了parent节点 不能再往上找了 */
return null;
}
/** 继续往上查找 */
node = parent;
}
找到存在sibling的父节点后,就需要继续寻找sibling节点,从其sibling节点向下找child,如果child是HostComponent或者是HostText,则找到兄弟节点了,直接返回。
需要注意,这里找的节点一定要是非Placement的,因为Placement的节点还没有挂载在真实dom树上,无法作为兄弟节点,所以在向下找的过程中,不论是什么节点 是不是Host节点,只要其存在Placment Flag,都要结束这条路的查找,重新循环找父节兄弟的下一个兄弟,(如果一个节点为Placment,其下所有节点一定还没有挂载,可以不用再找了)如图

ul节点还没有被挂载,无法被使用
看一个能找到sibling的例子,如下:
其中Div为更新阶段挂载上去的,那么此时 div.sibling 已经存在,但是因为其不是Host元素,继续往下找到ul,符合要求,返回作为sibling
getHostSibling函数实现如下:
TypeScript
/**
* 查找fiber的sibling host节点 (难点)
* 这里注意,sibling节点可能是不同级的
* 同时 对于Placement的节点,由于其和其child节点都还没放置 不能作为sibling节点
* 查找方向
* 1. 查看当前节点有没有sibling,如果有从sibling往下找(child) 如果child为hostComponent/HostTag 并且flag不为placement 则返回
* 如果查找的节点为placement 不论什么类型 查找和以下的节点都不能用 开始回溯
* 2. 回溯,找查找节点的parent,如果有sibling 则回到 (1) 查找其sibling 直到找到一个不为placement的hostCom/hostText为止
* 如果回溯的过程中,遇到了hostcomponent/hostroot 或者 null的节点 则直接返回null (因为回溯的过程中 一定走的都是非host节点 因为如果是host节点就肯定已经返回了)
* 如果回溯到过程中遇到host 那么一定是parent节点 或者已经找到hostRoot了 表示没找到
* @param fiber
*/
function getHostSibling(fiber: FiberNode): Element {
let node = fiber;
// 找sibling节点,没有找parent,如果遇到hostComponent / hostRoot 直接返回null
findSibling: while (true) {
while (node.sibling === null) {
const parent = node.return;
if (
parent === null ||
parent.tag === HostComponent ||
parent.tag === HostRoot
) {
/** 回溯的过程中 如果遇到了 hostComponent / hostRoot 说明找到了parent节点 不能再往上找了 */
return null;
}
/** 继续往上查找 */
node = parent;
}
// 执行到这里,说明存在sibling,移动node节点 -> sibling
node.sibling.return = node.return;
node = node.sibling;
// 找到sibling了 此时开始向下查找,这里要注意,寻找的节点必须满足
// 1. 是hostComponent / hostText
// 2. 不能是placement节点 如果不满足,返回到回溯阶段
while (node.tag !== HostComponent && node.tag !== HostText) {
// 都不是,如果此时为Placement 下面的不用看了 因为当前节点下的DOM还没挂载,直接回溯
if ((node.flags & Placement) !== NoFlags || node.child === null) {
continue findSibling; // 直接跳到最外层循环,回溯
}
// 向下寻找
node.child.return = node;
node = node.child;
}
// 运行到此处 找到hostCompoent/hostText了 看是不是placement
if ((node.flags & Placement) === NoFlags) {
return node.stateNode;
}
}
}
找到parent和sibling节点之后,就要执行插入操作,其调用的函数是 insertOrAppendPlacementNodeIntoContainer 这个名字一看就知道什么意思,就是完成插入操作的.
insertOrAppendPlacementNodeIntoContainer
这个函数就是把当前的Placement节点插入DOM树,但是需要考虑多种情况
-
如果带插入节点是个Host节点,那么直接调用parent.appendBefore() 或者在没有sibling的情况下调用parent.appendChild() 即可
-
如果不是Host节点,可能是Fragment或者FunctionComponent这样的虚拟节点,那么就需要遍历其所有子节点,如果把如果是Host的节点都插入,当然了如果是非Host节点需要递归的寻找,也是一个深度优先的过程。 如图,插入节点为FunctionComponent时,其插入路径如下:

实现如下: 其中insert函数被递归调用,保证每个host节点都被挂载
TypeScript
/**
* 插入或者追加finishwork节点到hostParent(container)中
* @param finishedWork
* @param hostParent
* @param hostSibling
*/
function insertOrAppendPlacementNodeIntoConatiner(
finishedWork: FiberNode,
hostParent: Container,
hostSibling?: Element
) {
// 这里需要注意 finishedWork 可能也不是HostComponet
if (finishedWork.tag === HostComponent || finishedWork.tag == HostText) {
if (hostSibling) {
hostParent.insertBefore(finishedWork.stateNode, hostSibling);
} else {
hostParent.append(finishedWork.stateNode);
}
} else {
// 如果finishwork不是host 比如是Fragment或者Function
// 需要遍历其子节点 并且添加
let child = finishedWork.child;
while (child !== null) {
insertOrAppendPlacementNodeIntoConatiner(child, hostParent, hostSibling);
child = child.sibling;
}
}
}
commitUpdate更新节点
更新节点逻辑相对节点, 对于文本节点,修改其nodeValue
对于host节点,修改其dom节点上的 __props属性 (具体在合成事件中讲)
TypeScript
/** 处理update副作用 */
function commitUpdate(fiber: FiberNode) {
if (fiber.tag === HostText) {
fiber.stateNode.nodeValue = fiber.memorizedProps.content;
} else {
updateFiberProps(fiber.stateNode, fiber.memorizedProps);
}
}
commitDeletion 删除节点
删除节点其实和inserOrAppendPlacementNodeIntoConatiner 一样,先找到其parent的DOM节点,如果当前待删除的节点为hostComponent或HostText 直接删除,如果是其他节点,就需要递归的去寻找其第一层子节点,并且删除
TypeScript
function commitDeletion(fiber: FiberNode, root: FiberRootNode) {
const container = getHostParent(fiber);
if (container) {
deleteNodeFromContainer(container, fiber, root);
}
}
/** 递归的方式删除节点 */
function deleteNodeFromContainer(
container: Container,
childToDelete: FiberNode,
root: FiberRootNode
) {
if (!container || !childToDelete) return;
if ((childToDelete.tag === HostComponent || childToDelete.tag === HostText)&&childToDelete.stateNode!==null) {
/** 如果是host节点,直接删除即可 */
if(container.contains(childToDelete.stateNode)){
container.removeChild(childToDelete.stateNode);
}
// 删除时,卸载Ref
if (childToDelete.tag === HostComponent) {
// HostComponent删除的时候 需要卸载Ref
saftyDetachRef(childToDelete);
}
} else {
/** 非host节点,递归删除 */
if (childToDelete.tag === FunctionComponent) {
/** 函数组件的情况下,需要收集Effect */
commitPassiveEffect(childToDelete, root, "unmount");
}
let deleteNodeChild = childToDelete.child;
while (deleteNodeChild !== null) {
deleteNodeFromContainer(container, deleteNodeChild, root);
deleteNodeChild = deleteNodeChild.sibling;
}
}
}
对于Ref和PassiveEffect 我们在后面说
执行完commitMutationEffects 会继续执行commitLayoutEffect, 其本质和MutationEffect一样,只不过mask换成了LayoutEffect = Ref 后面会细说Ref挂载更新卸载的流程
最后再次调度ensureRootIsScheduled
这样,我们就完成了简易的Commit流程!