前言
经过前面协调和调度的工作阶段,接下来就进入到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