React 源码揭秘 | commit流程

devtools/2025/2/28 23:40:32/

前面文章所描述的都发生在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流程!

 


http://www.ppmy.cn/devtools/163469.html

相关文章

会话对象 HttpSession 二、HttpSession失效

session失效有如下几个原因&#xff1a; session.invalidate()方法注销sessionsession超时 <session-config><!-- session的超时时间&#xff0c;以分钟为单位 --><session-timeout>1</session-timeout> </session-config>Cookie被禁用

Trae根据原型设计稿生成微信小程序密码输入框的踩坑记录

一、需求描述 最近经常使用Trae生成一些小组件和功能代码&#xff08;对Trae赶兴趣的可以看之前的文章《TraeAi上手体验》&#xff09;&#xff0c;刚好在用uniapp开发微信小程序时需要开发一个输入密码的弹框组件&#xff0c;于是想用Trae来实现。原型设计稿如下&#xff1a;…

第十章:服务器消费者管理模块

目录 第一节&#xff1a;代码实现 1-1.Consumer类 1-2.QueueConsumer类 1-3.QueueConsumerManger类 第二节&#xff1a;单元测试 下期预告&#xff1a; 服务器的消费者管理模块在mqserver目录下实现。 第一节&#xff1a;代码实现 创建一个名为mq_consumer.hpp的文件&#…

若依Vue,tab切换时实现缓存之前的查询条件、不刷新页面、不请求

1、菜单里面的路由地址和缓存 2、页面中对应路由地址&#xff0c;首字母大写驼峰命名

【Python爬虫(80)】当Python爬虫邂逅边缘计算:探索数据采集新境界

【Python爬虫】专栏简介:本专栏是 Python 爬虫领域的集大成之作,共 100 章节。从 Python 基础语法、爬虫入门知识讲起,深入探讨反爬虫、多线程、分布式等进阶技术。以大量实例为支撑,覆盖网页、图片、音频等各类数据爬取,还涉及数据处理与分析。无论是新手小白还是进阶开发…

SocketTool、串口调试助手、MQTT中间件基础

目录 一、SocketTool 二、串口通信 三、MQTT中间件 一、SocketTool 1、TCP 通信测试&#xff1a; 1&#xff09;创建 TCP Server 2&#xff09;创建 TCP Client 连接 Socket 4&#xff09;数据收发 在TCP Server发送数据12345 在 TCP Client 端的 Socket 即可收到数据12…

react使用拖拽,缩放组件,采用react-rnd解决

项目中需求&#xff0c;要求给商品图片添加促销标签&#xff0c;并且可拉伸大小&#xff0c;和拖拽位置 最后选择用react-rnd来实现 话不多说&#xff0c;直接上代码&#xff01;&#xff01;&#xff01; 1.在项目根目录下执行以下代码&#xff0c;引入react-rnd yarn add r…

【Arxiv 大模型最新进展】北大 Parenting 方法登场:参数魔法解锁检索增强语言模型新高度!

【Arxiv 大模型最新进展】北大 Parenting 方法登场&#xff1a;参数魔法解锁检索增强语言模型新高度&#xff01; &#x1f31f; 嗨&#xff0c;你好&#xff0c;我是 青松 &#xff01; &#x1f308; 自小刺头深草里&#xff0c;而今渐觉出蓬蒿。 NLP Github 项目推荐&#…