React源码系列(五):React的commit阶段

前言

经过前面协调和调度的工作阶段,接下来就进入到react渲染流程的最后一个工作阶段--commit阶段,就像我们写完代码(代指已经生成了一个新的wip fiber tree),接下来就是进入到提交代码操作了(代指操作dom tree)

在render阶段,可以异步可中断,但是在commit阶段,一旦开始,就不可以被打断,会同步执行直到完成。

从commitRoot方法开始进入commit阶段,可以分为两个部分

1、准备工作阶段

2、工作阶段主要包含下面三个阶段

2.1、beforeMutation

2.2、Mutation

2.3、Layout

1、准备工作阶段

通过commitRoot方法进入commitRootImpl方法,可以看到,在该方法首先会进入while循环,调用flushPassiveEffects,Passive标记对应的是useEffect的副作用操作。以及后面还有flushPassiveEffects方法的调用,判断条件是hostRootFiber自身是否有标记useEffect副作用操作或者hostRootFiber的子节点存在有标记useEffect副作用操作

scheduleCallback方法就是去创建一个task,最后会生成一个新的宏任务来异步处理副作用,这里处理副作用的方法就是flushPassiveEffects函数。

其实flushPassiveEffect方法就是专门用于页面渲染完成后来执行useEffect的回调,因为commit的渲染是同步执行,所以通过scheduleCallback方法调度后,保证flushPassiveEffect方法一定在本次dom更新完成后执行,而且还得保证本次commit阶段调度的useEffect必须在下一次commit执行之前先执行一次flushPassiveEffects,所以才会在每一次进入commit的时候,先执行一次flushPassiveEffects方法,去完成上一次可能存在的useEffect

总结:准备工作主要就是:清理可能存在的上一次的useEffect,然后调度本次commit阶段的useEffect,最后重置一些基本信息方便下个更新任务的内容挂载。

2、主要工作

2.1 beforeMutation阶段

回到commitRootImpl方法继续往下走,首先定义了2个变量

  • subtreeHasEffects:检查HostFiber的子孙元素是否存在副作用。
  • rootHasEffect:检查HostFiber自身是否存在副作用。

这两个变量的判断条件都和4个mask掩码相关

复制代码
BeforeMutationMask | MutationMask | LayoutMask | PassiveMask

只要满足其中任何一个mask,则变量结果为true。表示存在相关的副作用,需要执行commit逻辑。

ini 复制代码
// packages\react-reconciler\src\ReactFiberFlags.js

Placement // 代表当前Fiber或者子孙Fiber存在需要插入或者移动的dom元素或者文本【hostComponent,hostText】
Update // 1.触发class组件的mount/update生命周期钩子函数; 2.hostComponent发生属性变化; 3.hostText发生文本变化; 4.Fun组件定义了useLayoutEffect
Deletion // 当前节点存在删除操作
ChildDeletion // 子节点存在需要删除的dom或者文本【hostComponent,hostText】
ContentReset // 清空hostComponent,即DOM节点的文本内容
Callback // 调用this.setState时,传递了回调函数参数
Ref // ref引用的创建与更新
Snapshot // 触发class组件的getSnapshotBeforeUpdate方法【使用较少】
Passive // 触发函数组件的useEffect钩子

BeforeMutationMask = Update | Snapshot;
MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;
LayoutMask = Update | Callback | Ref | Visibility;
PassiveMask = Passive | ChildDeletion;

由上面代码可以看出,这4个掩码其实就是各种flags,而react应用的加载和更新基本都会存在各种副作用,所以默认都会进入这个判断,然后开始三个子阶段流程,同时源码中setCurrentUpdatePriority(DiscreteEventPriority),这个代表将当前更新的优先级设置为同步,代表了commit阶段的任务是立即执行不可中断的,接下来进入到commitBeforeMutationEffects。

2.1.1 commitBeforeMutationEffects

commitBeforeMutationEffects传递两个参数,一个是root,一个是finishedWork,root代表了当前react应用到根节点,也就是FiberRootNode,finishedWork代表了wip fiber tree的根结点,也就是HostFiberRoot。

ini 复制代码
BeforeMutationMask = Update | Snapshot;

BeforeMutationMask掩码表示beforeMutation阶段所需要执行的哪些副作用的类型

1、触发classComponent的getSnapshotBeforeUpdate,用于生成更新前到快照信息

2、hostComponent(也就是div span这种原生节点)发生属性变化,需要执行更新

3、hostText发生文本变化,需要执行更新

2.1.2 commitBeforeMutationEffects_begin

由代码可以看出,进入到commitBeforeMutationEffects_begin方法,会直接进入while循环,递归处理所有节点,如果该fiber的子节点存在BeforeMutation阶段相关的flags标记 且 child不为null; 则继续循环。直到找到第一个不存在beforeMutationMask的fiber,开始进入commitBeforeMutationEffects_complete

commitBeforeMutationEffects_begin方法的循环,是没有设置中断条件的,必须将所有的fiber node循环一遍。

为什么满足条件要继续循环,不满足条件才进入complete。因为满足条件代表它的子节点有需要处理的内容,而不是本节点,所以才需要进行下一轮的循环,不满足条件时则代表本节点有需要执行的副作用。

2.1.3 commitBeforeMutationEffects_complete

由代码看出,当执行commitBeforeMutationEffects_begin方法,遍历到不包含beiforeMutationMask的fiber node的时候,会进入commitBeforeMutationEffects_complete方法,进入归的阶段,在commitBeforeMutationEffects_complete方法内,也是直接进入while循环,对当前fiber node执行commitBeforeMutationEffectsOnFiber,然后检查有没有兄弟节点,如果有兄弟节点,将兄弟节点设置为下一个执行的fibe node退出complete的循环,回到begin的工作,如果不存在兄弟节点,则将下一个工作节点设置为父节点。

2.1.4 commitBeforeMutationEffectsOnFiber

由代码看出,在整个commitBeforeMutationEffectsOnFiber方法内部,其实也只是针对ClassComponent类型的fiber 和 HostRoot类型的fiber进行了处理,并且,也只有标记了Snapshot的才会执行,所以在beforeMutation阶段也就是处理了这2种类型的fiber

针对ClassComponent类型的fiber,首先判断了节点是否标记,并且不存在current fiber,这意味着是老的fiber节点并不存在,才会执行这个逻辑。

其实这段代码的意思就是在说,如果一个类组件定义了getSnapshotBeforeUpdate,则在这个阶段去执行,在react执行更新之前,会捕获类组件的一些信息,比如滚动位置等,然后将这些信息保存起来,可以在稍后传递给componentDidupdate执行。

针对HostRoot,其实就是将div#root的textContent清空掉,方便mutation阶段的渲染

hostFiber节点的Snapshot副作用是在completeWork工作中被标记的。

总结

在beforeMutation阶段,会做2件事,一个是类组件的getSnapshotBeforeUpdate方法的执行,以及处理hostRootFiber,清空#root容器的内容,方便mutation阶段的渲染。

2.2 Mutation阶段

回到commitRootImpl,进入commitMutationEffects方法

2.2.1 commitMutationEffects

commitMutationEffects接收三个参数,root就是指FiberRootNode,也就是整个react应用的根结点,finishedWork也就是wip HostRootFiber,committedLanes代表不同优先级的更新

ini 复制代码
MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;

这是在MUtation阶段的mask,重点就是针对fiber节点上dom的处理,然后将最终的dom渲染到页面上

2.2.2 commitMutationEffectsOnFiber

可以看出在commitMutationEffectsOnFiber方法内,主要是对不同类型的fiber node进行处理,

  • FunctionComponent:函数组件。
  • ClassComponent:类组件。
  • HostComponent:DOM节点。
  • HostText:文本节点。
  • HostRoot:HostFiber根节点。

从上面的类型看,都是相同的处理逻辑

scss 复制代码
// 1,删除DOM
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 2,插入DOM
commitReconciliationEffects(finishedWork);
// 3,更新(DOM内容)

由这张图看出,mutation阶段的执行顺序也是自上而下开始遍历,执行删除dom操作,然后到了最底层子节点,开始自下而上执行插入和更新dom操作

2.2.3 recursivelyTraverseMutationEffects

代码看出,在recursivelyTraverseMutationEffects方法内,主要做了2个工作

1、判断当前fiber是否存在deletions删除标记,如果存在则需要循环deletions,删除子节点对应的dom元素内容

2、从hostRootFiber.child 开始,也就是从app开始,遍历fiber tree,递归调用commitMutationEffectsOnFiber

执行删除逻辑的时候,会去执行组件的卸载生命周期钩子,componentWillUnmount,会去执行函数组件的useEffect、useLayoutEffect等hook的destory销毁方法

2.2.4 commitReconciliationEffects

commitReconciliationEffects方法就是执行DOM的插入或者移动操作,判断当前Fiber节点是否存在Placement标记,存在就会执行commitPlacement方法,执行相关的DOM的操作。

2.2.5 commitPlacement

代码看出,能够执行Placement操作的,只有HostComponent节点和HostRootFiber节点,对于HostComponent来说,就是常规的dom插入和移动,调用的是原声的DOM方法

scss 复制代码
// 原生DOM操作
parentNode.appendChild()
parentNode.insertBefore()

总结

在mutation阶段的主要工作

1、针对DOM的增删改操作,这个是mutation阶段的主要工作,最后将构建完成的离屏dom tree渲染到页面上

2、类组件componentWIllUnmount方法的执行 以及useEffect 和 useLayoutEffect卸载函数的执行

3、重置ref对象

2.3 fiber tree 的切换

之所以选择这一时机切换fiber tree, 是因为对于ClassComponent,当执行componentWillUnmount(mutation阶段)的时候,current fiber tree 仍对应UI中的树,当执行componentDidMount/Update(Layout阶段)的时候,current fiber tree就对应本次更新的fiber tree了,也就是原来的 wip fiber tree 变成了 current fiber tree

2.4 Layout阶段

commitLayoutEffects函数接收三个参数,finishedWork代表初始的时候 hostRootFiber, root代表FiberRootNode

ini 复制代码
LayoutMask = Update | Callback | Ref | Visibility;

LayoutMask就是代表Layout阶段所需要执行的哪些副作用类型:

1、类组件的componentDidMount/componentDidUpdate生命周期钩子函数的执行。

2、类组件调用this.setState时传递的callback回调函数的会被保存到Fiber节点的updateQueue属性中在这里执行。

3、执行函数组件的useLayoutEffect hook回调。

可以说Layout阶段的主要内容:就是在DOM渲染完成后,执行函数组件和类组件定义的一些callback回调函数

2.4.1 commitLayoutEffects

在Layout阶段,还是和beforeMutation和Mutation阶段一样,采用深度优先遍历的方式,根据wip.tag处理每个fiber node

2.4.2 commitLayoutEffects_begin

和原来mutation逻辑一样,根据fiber.tag分别处理不同类型的fiber node,commitLayoutEffects_begin接收三个参数,root代表FiberRootNode,finishedWork代表hostRootfiber, 如果子级存在副作用,则begin递归向下查找,直到找到自身LayoutMask的节点,进入commitLayoutMountEffects_complete

2.4.3 commitLayoutMountEffects_complete

从自身存在LayoutMask副作用的节点开始,向上执行副作用

2.4.4 commitLayoutEffectOnFiber

commitLayoutEffectOnFiber主要就是针对不同的fiber.tag进行不同的逻辑处理

1、针对函数组件,同步执行useLayoutEffect的回调

2、针对类组件,根据当前fiber是否存在对应的current fiber来区分是mount阶段还是update阶段

2.1 mount阶段:执行当前类组件的componentDidMount生命周期函数

2.2 update阶段:执行当前类组件的componentDidUpdate生命周期函数

总结

在Layout阶段主要做了以下工作

1、执行函数组件useLayoutEffect的回调函数

2、执行类组件的生命周期函数 componentDidMounnt/Update

3、执行ref对象绑定,更新ref

相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端