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、准备工作阶段

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

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

通过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阶段

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

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

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

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

BeforeMutationMask | MutationMask | LayoutMask | PassiveMask

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

// 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。

BeforeMutationMask = Update | Snapshot;

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

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

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

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

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

2.1.2 commitBeforeMutationEffects_begin

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

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

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

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

2.1.3 commitBeforeMutationEffects_complete

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

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

2.1.4 commitBeforeMutationEffectsOnFiber

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

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

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

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

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

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

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

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

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

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

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

总结

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

2.2 Mutation阶段

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

回到commitRootImpl,进入commitMutationEffects方法

2.2.1 commitMutationEffects

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

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

MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;

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

2.2.2 commitMutationEffectsOnFiber

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

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

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

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

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

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

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

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

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

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

2.2.3 recursivelyTraverseMutationEffects

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

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

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

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

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

2.2.4 commitReconciliationEffects

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

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

2.2.5 commitPlacement

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

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

// 原生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阶段

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

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

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

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

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

2.4.2 commitLayoutEffects_begin

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

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

2.4.3 commitLayoutMountEffects_complete

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

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

2.4.4 commitLayoutEffectOnFiber

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

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

原文链接:https://juejin.cn/post/7329341975207936038 作者:网页粉刷匠

(0)
上一篇 2024年1月30日 上午10:05
下一篇 2024年1月30日 上午10:17

相关推荐

发表回复

登录后才能评论