前面文章所描述的都发生在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
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
export const PassiveEffect = 0b0001000;
export const PassiveMask = PassiveEffect | ChildDeletion;
因为useEffect不仅仅是在监听值变动时执行,在组件卸载时,也会执行其返回的函数
PassiveMask为这两个Flags的合集
commit准备工作
在render阶段结束之后,此时已经有一棵完整的待更新Fiber树了。
此时需要为commitRoot做一些初始化工作
1. root.finishedWork用来记录已经生成的待Commit的Fiber树,由于render阶段结束之后,workInProgress已经为空了,这个值需要用root.current.alternate获得。
2. root.finishedLane 用来记录当前处理的lane,需要用wipRootRenderLane赋予
在给这两个属性赋值后,需要吧wipRenderLane置为NoLane
最后调用commitRoot函数,开始commit阶段
// performWorkOnRoot
//任务完成 收尾 commit
// 设置root.finishedWork
root.finishedWork = root.current.alternate;
root.finishedLane = lane;
// 设置wipRootRenderLane = NoLane;
wipRootRenderLane = NoLane;
// 进入commit阶段
commitRoot(root);
commitRoot - 开启commit流程
commitRoot函数用来开启commit阶段,其要做的事情为:
1. 拿到root.finishedWork & root.finishedLane 并且把这两个值置为空, 并且调用markRootFinished把当前处理的lane从root.pendingLanes去除,表示已经完成本lane的处理。
2. 检测finishedWork根节点是否包含PassiveEffect或者其子节点subtreeFlasg是否包含PassiveMask, 如果包含,则开启Passive阶段,并且将Passive阶段处理函数推入scheduler,在Mutation阶段结束之后调度运行。
3. 检测finishedWork的flags和subTreeFlags是否包含 (PassiveMask | MutationMask) 因为Mutation阶段除了要处理阶段的插入删除更新Ref,还需要收集Effect。
4. 如果包含Flag,说明需要commit 开启commitMuationEffect
5. 把root.current赋finishedWork
6. 开启Layout阶段,调用commitLayoutEffect 用来挂载Ref
7. 重新开启调度流程,更新结束
实现如下:
/** 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去掉当前的lanemarkRootFinished(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 RefcommitLayoutEffects(finishedWork, root);// 确保可以继续调度ensureRootIsScheduled(root);
}
commitMutationEffect - Mutation阶段
commitMutationEffect是高阶函数commitEffect返回的函数,其调用如下:
export const commitMutationEffects = commitEffect("mutation",MutationMask | PassiveMask,commitMutationEffectsOnFiber
);
其中commitEffect是个高阶函数,需要传入
1. 当前commit的阶段,mutation|layout,
2. 需要检测的Mask
3. 处理函数callback
这个函数的作用就是返回一个处理函数,这个处理函数会深度优先遍历传入的finsihedWork 树,在向下递的阶段,每一次都查看当前节点的子节点是否还有对应需要检测的Mask,如果有就继续往下找,如果没有就停止,开始归的过程,这样能减少不必要的遍历。在归的过程中,调用callback函数完成对Flags的处理。
/** 高阶函数 用来处理Effect */
function commitEffect(phrase: "mutation" | "layout",mask: Flags,callback: CommitCallback
): CommitCallback {/** 递归,DFS 找到最深的无subflags的节点 下面的不需要commit了 因为没有副作用 */return (finishedWork, root) => {// DFSlet 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处理函数
/** 用来处理 Mutation副作用 [Placement | Update | ChildDeletion // TODO PassiveEffect] */
const commitMutationEffectsOnFiber: CommitCallback = (finishedWork, root) => {// 处理每个节点的Effect// 获取节点的flagsconst flags = finishedWork.flags;// 处理placementif ((flags & Placement) !== NoFlags) {// 存在PlacementcommitPlacement(finishedWork);// 去掉副作用flag// 去掉某个flag: 0b0111&(~0b0100) => 0b0111&0b1011=> 0b0011 去掉了 0b0100finishedWork.flags &= ~Placement;}// 处理Updateif ((flags & Update) !== NoFlags) {commitUpdate(finishedWork);finishedWork.flags &= ~Update;}// 处理ChildDeletionif ((flags & ChildDeletion) !== NoFlags) {const deletion = finishedWork.delections;deletion.forEach((deleteOldFiber) => {commitDeletion(deleteOldFiber, root);});finishedWork.flags &= ~ChildDeletion;}// 处理收集passiveEffectif ((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树上,插入节点需要找到要插入的父节点,以及要插入到父节点的哪个位置,即找到兄弟节点。
需要注意的点是:
1. 只有HostComponent节点才能作为parent节点被插入
2. 只有HostComponent节点或HostText节点才可以作为被插入节点的兄弟
由于函数节点 Fragment节点 Memo节点等仅在Fiber中体现,不存在于真实DOM中, 所以在寻找父节点和兄弟节点时需要注意。
/** 处理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节点
/** 获取HostParent* 获取当前节点的HostComponent/HostRoot parent*/
function getHostParent(fiber: FiberNode): Element {let node = fiber.return;while (node !== null) {if (node.tag === HostComponent) {// host component 返回其stateNodereturn 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的父节点
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函数实现如下:
/*** 查找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 直接返回nullfindSibling: 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节点 -> siblingnode.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了 看是不是placementif ((node.flags & Placement) === NoFlags) {return node.stateNode;}}
}
找到parent和sibling节点之后,就要执行插入操作,其调用的函数是 insertOrAppendPlacementNodeIntoContainer 这个名字一看就知道什么意思,就是完成插入操作的.
insertOrAppendPlacementNodeIntoContainer
这个函数就是把当前的Placement节点插入DOM树,但是需要考虑多种情况
1. 如果带插入节点是个Host节点,那么直接调用parent.appendBefore() 或者在没有sibling的情况下调用parent.appendChild() 即可
2. 如果不是Host节点,可能是Fragment或者FunctionComponent这样的虚拟节点,那么就需要遍历其所有子节点,如果把如果是Host的节点都插入,当然了如果是非Host节点需要递归的寻找,也是一个深度优先的过程。 如图,插入节点为FunctionComponent时,其插入路径如下:
实现如下: 其中insert函数被递归调用,保证每个host节点都被挂载
/*** 插入或者追加finishwork节点到hostParent(container)中* @param finishedWork* @param hostParent* @param hostSibling*/
function insertOrAppendPlacementNodeIntoConatiner(finishedWork: FiberNode,hostParent: Container,hostSibling?: Element
) {// 这里需要注意 finishedWork 可能也不是HostComponetif (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属性 (具体在合成事件中讲)
/** 处理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 直接删除,如果是其他节点,就需要递归的去寻找其第一层子节点,并且删除
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);}// 删除时,卸载Refif (childToDelete.tag === HostComponent) {// HostComponent删除的时候 需要卸载RefsaftyDetachRef(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流程!