React的调和过程(Reconcilliation)

13 篇文章 0 订阅

Reconciliation 是实现UI更新的一个过程。目的就是在用户无感知的情况下降数据的更新体现到UI上。

触发调和的方式
  • ReactDom.render() 函数 和 ReactNativeRender.render()函数
  • setState()函数
  • forceUpdate()函数的调用
  • comopnentWillMount 和 componentWillReceiveProp 中直接修改了state(地址)
  • hooks中的useReducer 和 useState 返回的钩子函数
virtualDom

虚拟Dom是一种编程概念,也就是UI的理想表示保存在内存中,并通过如 ReactDOM 之类的库于真实DOM进行同步

ReactElement

ReactElement 由 React.createEement这个函数创建。本质也是一个JS Object。也就是,虚拟Dom的具象化的东西。
具体和 ReactElement,可以看这个。
https://blog.csdn.net/qq_44786519/article/details/108911919

当然ReactElement 不会被复用,每次调和都是重新生成,他的作用也是用来生成 Fibe 节点。

Fibe 节点

Fibe 节点是 React 调和过程中最小的工作单元,其内记录了组件数据,要执行的任务,以及schedule(时间片)任务调度相关的信息。

Fide节点具有以下特性
  • mutable,可复用:每一个 ReactElement 都会对应用一个 Fibe 节点,并不是每次调和都需要重新创建 Fibe 节点
  • 更新队列:对于组件,updateQueue队列记录state更新操作,当调用setState后,这个state的更新操作会进入updateQueue队列,等待执行。在一次调和过程中,会执行updateQueue队列中的所有的state更新,计算出最后的state。对于Dom,调和时,会使用updateQueue记录dom需要更新的属性。
  • 工作标签:每个Fibe节点都有一个工作标签,标识fibe的类型。目前raect中有19种工作标签,调和过程中,raect会根据fibe的工作标签,执行不同的任务,在fibe中也有type属性,该属性值来源于ReactElement,表示与fibe关联的组件函数或组件class,并不用于标识fibe类型
  • 树状:fibe节点通过父return,子child,兄sibling属性关联,组成一课Fibe。
  • 两颗树:在调和过程中会存在两颗FIbe节点数,一颗称之为current记录生成当前Dom的数据,另一个数称为“workProgress”记录即将要更新Dom的数据,两颗树间相应的fibe节点
  • 外部关联:Fibe节点和通过stateNode属性来关联其相应的Dom节点,或者组件instance同时增加了相关属性关联到其对应的Fibe节点上
  • 保存组件数据:Fibe节点会记录组件的state数据和props属性,‘current’树中的Fibe节点记录的当前state和props,“workinProgress”树中的Fibe节点记录的是即将要更新的state和props。
  • 记录任务:Fibe节点通过effectTag来记录当前节点在调和过程的commit阶段需要执行的任务,包括Dom增删改,生命周期函数调用,ref赋值等。effectTag以12位的二进制书记表示,每一位表示一种任务。在调和过程的render阶段,添加任务,commit阶段,执行任务。
  • 线性任务链 effect list:在一次调和过程中,只有部分Fibe节点在commit阶段会涉及到任务,而且这些节点的任务需要按序执行。在调和过程的render阶段,通过“firstEffect”,“nextEffect”,“lastEffect”属性将需要执行任务的fibe节点有序的链接在一起,跳过无关的fibe,形成一个精简有序的线形列表effect list,使得commit阶段能高效迭代执行任务。(effect list 是调和过程能高效执行的法宝之一,迭代一个精简过的线性list比迭代一歌完整的树更快)

两颗Fibe Node树

在这里插入图片描述

两颗树的创建时间

第一次render时,调和过程会根据ReactElement创建第一课树 current fibe tree,并通过current属性将其挂载在FibeRoot对象中,FibeRoot是react创建的容器,与应用定义的容器Dom相关联。
在后续update时,每次的调和过程都会去快速的构建一颗 Workinprogress tree。(怎么快速构建?也是React高效的法宝之一)

两个树的关联

current和workInprogress 两颗树具有相似的结构,根节点都是HostRoot,由React内部自动生成的一类fibe节点,其return属性为null,通过child属性挂起应用的其实fibe节点。两颗树之间的关联节点通过alternate属性指向对方,这样在调和过程中,可以轻易的在两颗树间切换。

Instance & Fibe & DOM关联

同时,Fibe节点可以轻松的通过stateNode属性获取到关联的class组件instance或者Dom节点,Fibe节点类型为class Component时,stateNode指向组件Instance;若为HostComponent,stateNode指向Dom。class组件Instance
中也有一个_reactInternalFiber属性可以快速获取相关fibe节点,若组件是在初次render时就实例化了,则指向的shicurrent树的fibe节点,若是在更新调和时实例化的,则指向的是workInprogress树的fibe节点,但调和过程结束后,workInprogress 树也会变为current树。Instance&Fibe&DOM之间的这种友好关联,使得Instance上触发的更新,都能快速的反应到相应的Fibe上,触发调和过程;React可根据Fibe快速更新相关DOM。

QA;什么是Instance?Instance是面向对象编程范式的产物,在这里表示一个class组件其对应的class的一个实例,一个class组件可以在多个地方使用,其class也就可以被实例化多次。在ClassComponent类型的Fibe被初次创建时,其type 上记录的class,会被实例化。

线性任务链effect list

raect在调和过程的render阶段不可以执行,会影响其他组件的任务统称为 effect,也就是说effect定义的是fibe在commit阶段需要执行的任务。没类fibe节点都可以拥有多个不同的effect,使用effectTag标记。对于HostComponent fibe节点,effect可以是Dom的增删改;对于ClassComponent fibs节点,effect可以是生命周期函数的调用,ref的更新;对于FunctionComponent fibe节点,effect可以是effect hook函数的调用;其他类型的fibe还会有其他类型的任务。
effect list 是一个有序的线性list,其起点在HostRoot fibe节点,而后先连接child后连接parent。effect list中的fibe可以是跨current和workinpropgress树的,因为在调和的commit阶段,有些任务是要在current fibe树上操作的,例如删除fibe,删除Dom操作。

调和过程中有两个fibe节点会有side effect,生成的effect list,起点在workinprogress 树的HostRooot fibe中,通过firstEffect首先指向有Dom更新的子节点span fibe,然后通过 nextEffect 指向父节点Counter fibe,跳过了没有effect的button fibe。effect list的顺序是自下而上的,首先完成子节点的effect,再去完成父节点的effect。

调和过程

React的调和过程分为两个阶段RenderRoot和CompleteRoot,第一阶段称为render阶段,主线是构建workInprogress Fibe节点数,准备好线性任务链effect list。在这个阶段的最后,workInprogress Fibe tree 会变成finishedWork fibe tree,以finishedWork属性挂载到FibeRoot对象里,提供给第二个阶段使用。第二个阶段又被称为commit阶段,主要目标是根据线性任务链完成finishedWork Fibe 节点数中记录的任务,实现UI的更新。

Render阶段

render阶段以一个Fibe节点为单元,采用递归的方式,实现workInprogress的树的快速搭建。搭建过程中还会实现如下功能:

  • 更新state和props
  • 调用部分生命周期钩子函数
  • 新旧children diff,标记更新
  • 找出DOM需要更新的属性,并标记更新
  • 预生成新增的Dom对象,先挂载在fibe上
递归流程

在源码中,递归流程从FiberRoot开始,分为递归前进段,边界条件和递归返回段。
递归前进段
递归前进段由上往下,主要工作包括:

  • 搭建子节点(调和子节点)
    针对初次创建的调和过程,current树不存在,所有的fibe节点都需要根据 ReactElement 创建。在以后针对更新的调和过程,current树已经存在,workProgress树中的子节点的搭建方式有多种,包括直接使用current树节点,克隆current数的fibe节点修改属性,复用current已有的alternate节点修改属性,根据 ReactElement 创建Fibe四种方式;为实现高效搭建,调和过程有如下策略:
    所以,子节点的搭建方式上的优先级,直接使用current树节点>复用 current alternate节点>clone
    current 节点>从ReactElement创建。总之,尽可能的复用,减少创建。
    根据以上的策略,当某个组件发生更新时,调和过程在构建workInprogress树时会直接复用current上没有更新的分支fibe树,快速的进入有跟新的fibe节点。

在这里插入图片描述
如图所示,在这颗树中Counter fibe节点发生了更新,调和过程从HostRoot节点开始,往下递归;遇到page fibe节点,自身没有更新但子节点有跟新,会直接clone current 树上的page fibe节点,同时将child关系保留。遇到 UserInfo fibe 节点时,节点无更新,其子节点也无跟新,则直接使用整棵子节点树,直接范湖进入Counter fibe 节点树,该节点有跟新,开始解析相关跟新操作。虽然这课树大,但是搭建过程从HootRoot开始只需要3次递归调用,就可以进入目标跟新节点

  • 对于没有跟新且子节点也没有跟新的节点,则会直接使用current树上的节点及其下所有子节点
  • 对于当前节点没有跟新,但其子节点有跟新,会采用clone节点或者复用current已有的alternate节点,修改属性后使用;
  • 对于有更新的节点,生成ReactElement(class组件直接调用组件的render函数,函数组件直接调用elementType记录的生成函数),与current树上的子fibe节点对比,若存在相同的key和type的,则clone修改属性后使用,若没有,则根据ReactElement重新生成子节点,原current上对应的子节点树全部删除(这就是Diffing算法的概要)
  • 对于有更新的fibe节点标记 effectTag。对于有跟新的fibe节点,在搭建时,会记录相应的effectTag,搭建子节点时,对于新增和移动未知的fibe节点,会标记 “palcement” tag,对于 current 上需要的删除的fibe,会标记 “deletion” tag
  • 对于有更新的ClassComponent类型 fibe 节点,同时也会执行需要在render阶段执行的钩子函数(componentwillMount,componentwillReceiveProps,getDerivedStateFromProps,shouldComponentUpdate,componentWillUpdate,render)
递归边界条件

递归前进段到达叶子节点,即返回。
每一次递归前进都会计算下一次递归单元,计算规则如下:

  • 若当前节点存在于子节点,则返回第一个子节点的地址,继续向下递归。
  • 若已到叶子节点,不存在子节点,进入递归返回段。首先完成当前子节点在返回段的任务,在返回下一个兄弟节点进入递归;若不存在兄弟节点,继续完成父节点在返回段任务,在返回父节点的下一个兄弟节点进入递归,若父节点不存在下一个兄弟节点,则继续完成祖父节点返回段任务,返回祖父节点的下一个兄弟节点进入递归,依次类推。
  • 当返回到hostRoot节点时,结束递归流程。

递归返回段,主要是针对Host* 类型的 进行Diff操作,对于属性有更新的fibe,标记 “update” effectTag,同时将props的更新记录到updateQueue中;对于新增的host类型fibe,会生成相关的dom对象,通过stateNode,先挂在fibe上。

Commit阶段

这个阶段相比第一个阶段,任务很轻,就是遍历effect list,执行side effects,将数据的更新体现到UI上,这个阶段会涉及UI的更新。

  • 1.执行所有的effect list 节点的生命周期函数getSnapshotBeforeUpdate
  • 2.执行所有的effect list 节点Dom更新,ref删除,以及 componentWillUnmount 生命周期函数的调用
  • 3.将workFinished tree设置为 current tree
  • 4.执行所有的effect list 节点的 mutation 生命周期函数,ref的添加

总结

调和过程,将任务重的diff工作都放在了第一个阶段,第二个阶段快速的更新页面。这源于React提高性能的一个策略:异步调度。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值