目录

什么是Reconciliation

调和的目的

触发调和过程的方式

调和过程涉及的数据结构

ReactElement

Fibe 节点

两棵Fibe Node树

两棵树的创建时间

两棵树关联

Instance & Fibe & DOM 关联

线性任务链effect list

调和过程

Render阶段(renderRoot)

递归流程

Commit阶段(CompleteRoot)

尾声


什么是Reconciliation

在React的官网在解释virtualDom时,有这样一段话:

"The virtual DOM (VDOM) is a programming concept where an ideal, or “virtual”, representation of a UI is kept in memory and synced with the “real” DOM by a library such as ReactDOM. This process is called reconciliation."

Reconciliation 是实现UI更新的一个过程。这里翻译成“调和”;思考: 为什么叫调和??

在react的官网中也有一篇high-level的文章介绍Reconciliation,文中主要介绍了调和过程的动机,调和过程中的diff算法,但是对调和过程是一个怎样的流程,并没有介绍到,本篇重点探索Reconciliation的实现流程以及涉及的数据结构。

调和的目的

Reconciliation 的目的是在用户无感知的情况下将数据的更新体现到UI上。

触发调和过程的方式

在React 中有以下几种操作会触发调和过程:

  • ReactDom.render()函数 和ReactNativeRenderer.render()函数
  • setState()函数
  • forceUpdate()函数的调用
  • componentWillMount 和componentWillReceiveProp 中直接修改了state(地址)
  • hooks 中的useReducer 和 useState 返回的钩子函数

调和过程涉及的数据结构

接触React时,首先接触的是virtualDom这个词,会好奇什么是virtualDom。开端的引文已经解释,virtualDom只是一种编程理念,在react中若一定要将他与某种数据相关联,那应该是ReactElement 和fibe,fibe更合适。

ReactElement

ReactElement 由 React.createElement(type, config, children) 函数创建(在编写React应用时,采用的JSX语法,只是该函数的语法糖)。 其本质就是一个Js Object,具有如下属性(不包含DEV环境属性)。

const element = {// This tag allows us to uniquely identify this as a React Element$$typeof: REACT_ELEMENT_TYPE,// Built-in properties that belong on the elementtype: type,key: key,ref: ref,props: props,// Record the component responsible for creating this element._owner: owner, // fibe node or null
};

其中:

  • $$typeof: 固定值,用于标识该对象是一个ReactElement
  • type: 表示 ReactElement的类型,用于判断如何创建对应的fibe节点。对于Class组件是其class,对于函数组件是该函数,对于原生DOM是字符串,例如“Input”,对于React 原生提供的组件如Fragment,是React内部提供的Symbol
  • key: 是config中指定的key,默认值为null
  • ref:是config中指定的ref,默认值为null
  • props:该element的属性,其内可使用children属性描述这个element的children信息,children 可以是一个ReactElement,也可以是一个ReactElement数组。(注意这里的children属性并不是指的React Component的内容)
  • _owner:用于记录创建这个ReactElement的Fibe 节点 *

例如,对于如下函数组件Container,使用div包了一个Counter计数器, Counter 是一个Class组件:

    function Container(){return <div><Counter/></div>}

Container 函数返回的ReactElement就是:

    {$$typeof: REACT_ELEMENT_TYPE,type: 'div',key: null,ref: null,props:{children:{$$typeof: REACT_ELEMENT_TYPE,type: Counter,key: null,ref: null,props:{}}},}

ReactElement不会被复用,每次调和都是重新生成, 其作用是用来生成下面的Fibe 节点,参考源码函数:createFiberFromElement

Fibe 节点

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

Fiber = {tag: WorkTag,key: null | string,elementType: any, // 从ReactElement 获取type: any,   // 一般与elementType相同,只对于lazy component 为nullstateNode: any,return: Fiber | null,child: Fiber | null,sibling: Fiber | null,index: number,ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,pendingProps: any, // This type will be more specific once we overload the tag.memoizedProps: any, // The props used to create the output.updateQueue: UpdateQueue<any> | null,memoizedState: any,contextDependencies: ContextDependencyList | null,mode: TypeOfMode,effectTag: SideEffectTag,nextEffect: Fiber | null,firstEffect: Fiber | null,lastEffect: Fiber | null,expirationTime: ExpirationTime,childExpirationTime: ExpirationTime,alternate: Fiber | null,
};

Fibe 节点具有以下特性:

  • mutable,可复用:每一个ReactElement 都会对应用一个Fibe 节点,但并不是每次调和都需要重新创建Fibe 节点,Fibe节点是一个mutable 对象,可以进行更新然后复用。在调和过程中,fibe 节点是更新复用,还是重新创建,与其type 和 key 属性相关。
  • 更新队列:对于组件,updateQueue队列记录state更新操作,当调用setState后,这个state的更新操作会进入updateQueue队列,等待执行。在一次调和过程中,会执行updateQueue队列中的所有的state更新,计算出最后的state。对于Dom,调和时,会使用updateQueue记录dom需要更新的属性。
  • 工作标签:每个Fibe节点都有一个工作标签tag,标识fibe的类型。目前react中有19种工作标签,调和过程中,react会根据fibe的工作标签,执行不同的任务. 在fibe中也有一个type属性,该属性值来源于ReactElement,表示与fibe关联的组件函数或组件class,并不用于标识fibe类型。

    export const FunctionComponent = 0;
    export const ClassComponent = 1;
    export const IndeterminateComponent = 2; // Before we know whether it is function or class
    export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
    export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
    export const HostComponent = 5;
    export const HostText = 6;
    export const Fragment = 7;
    export const Mode = 8;
    export const ContextConsumer = 9;
    export const ContextProvider = 10;
    export const ForwardRef = 11;
    export const Profiler = 12;
    export const SuspenseComponent = 13;
    export const MemoComponent = 14;
    export const SimpleMemoComponent = 15;
    export const LazyComponent = 16;
    export const IncompleteClassComponent = 17;
    export const DehydratedSuspenseComponent = 18;
  • 树状:fibe 节点间通过父return,子child,兄sibling 属性关联,组成一棵Fibe 节点树。

  • 两棵树:在调和过程中会存在两棵Fibe 节点树,一棵称之为“current”记录生成当前Dom的数据,另一棵称为“workInProgress”记录即将要更新Dom的数据。两颗树间相应的fibe 节点通过“alternate”属性相互关联。

  • 外部关联:Fibe 节点通过“stateNode”属性来关联其相应的Dom节点,或者组件instance。同时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比迭代一棵完整的树更快)

    // Don't change these two values. They're used by React Dev Tools.
    export const NoEffect = /*              */ 0b000000000000;
    export const PerformedWork = /*         */ 0b000000000001;// You can change the rest (and add more).
    export const Placement = /*             */ 0b000000000010;
    export const Update = /*                */ 0b000000000100;
    export const PlacementAndUpdate = /*    */ 0b000000000110;
    export const Deletion = /*              */ 0b000000001000;
    export const ContentReset = /*          */ 0b000000010000;
    export const Callback = /*              */ 0b000000100000;
    export const DidCapture = /*            */ 0b000001000000;
    export const Ref = /*                   */ 0b000010000000;
    export const Snapshot = /*              */ 0b000100000000;
    export const Passive = /*               */ 0b001000000000;// Passive & Update & Callback & Ref & Snapshot
    export const LifecycleEffectMask = /*   */ 0b001110100100;// Union of all host effects
    export const HostEffectMask = /*        */ 0b001111111111;export const Incomplete = /*            */ 0b010000000000;
    export const ShouldCapture = /*         */ 0b100000000000;

两棵Fibe Node树

两棵树的创建时间

第一次render时,调和过程会根据ReactElements创建第一棵树 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;若为Host Component,stateNode指向Dom。 class组件Instance中也有一个_reactInternalFiber属性可以快速获取相关fibe节点,若组件是在初次render时就实例化了,则指向的是current树的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

在react Effect Hook的官网介绍中有一段关于“effect”的说明:

"You’ve likely performed data fetching, subscriptions, or manually changing the DOM from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering."

react 将在调和过程的render阶段不可以执行,会影响其他组件的任务统称为“effect”,也就是说effect定义的是fibe在commit阶段需要执行的任务。每类fibe节点都可以拥有多个不同的effect,使用effectTag标记。对于HostComponent fibe节点,effect可以是Dom的增删改;对于ClassComponent fibe节点,effect可以是生命周期函数的调用,ref的更新;对于FunctionComponent fibe 节点,effect可以是effect hook函数的调用;其他类型的fibe还会有其他类型的任务。

effect list 是一个有序的线性list,其起点在HostRoot fibe 节点,而后先链接child 后链接 parent。 effect list 中的fibe 可以是跨current和workinprogress 树的,因为在调和的commit阶段,有些任务是要在current fibe树上操作的,例如删除fibe,删除Dom操作。

例如, 对于如下组件,点击按钮后,更新点击次数,更新完后在console中打印“updated”信息。

class Counter extends React.Component{state={count:0}addCount = ()=>{const count = this.state.count+1;this.setState({count})}componentDidUpdate(){console.log("updated")}render(){return [<button onClick={this.addCount}>点击</button><span>点击次数:{this.state.count}</span>]}
}ReactDom.render(<Counter />, document.getElementById("container"));

在调和过程中,有两个fibe节点会有side effect,生成的effect list 如下图红色链接线所示,起点在 workinprogress 树的HostRoot 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阶段(renderRoot)

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

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

递归流程

在源码中,递归流程从FiberRoot 开始,分为递归前进段,边界条件,和递归返回段。

递归前进段

递归前进段由上往下,主要工作包括:

  • 搭建子节点(又称调和子节点)

    针对初次创建的调和过程,current树不存在,所有的fibe节点都需要根据ReactElement创建。在以后针对更新的调和过程,current树已经存在,workInProgress树中的子节点的搭建方式有多种,包括直接使用current树节点,克隆current树的fibe节点修改属性,复用current 已有的alternate节点修改属性,根据ReactElement 创建Fibe四种方式;为实现高效搭建,调和过程有如下策略:

    由上可见,在子节点的搭建方式上的优先级,直接使用current树节点 > 复用 current alternate节点 > clone current 节点 > 从ReactElement 创建。总之,尽可能的复用,减少创建。

    根据上面的策略,当某个组件发生更新时,调和过程在构建workInprogress树时会直接复用current上没有更新的分支fibe树,快速的进入有更新的fibe节点。 如下图所示,这颗复杂的fibe tree中,Counter fibe 节点发生了更新,调和过程从 HostRoot 节点开始,往下递归;遇到 page fibe节点,自身没有更新但子节点有更新,会直接clone current 树上的page fibe节点,同时将其child关系保留。 遇到 UserInfo fibe 节点时,节点无更新,其子节点也无更新,则直接使用整棵子节点树。直接返回进入 Counter fibe 节点树,该节点有更新,开始解析相关更新操作。虽然这棵树很大,但是搭建过程从HootRoot开始只需要3次递归调用,就可进入目标更新节点。

    1. 对于没有更新且子节点也没有更新的节点,则会直接使用current树上的节点及其下所有子节点(由此可见,两颗树在物理空间上是交错在一起的。)

    2. 对于当前节点没有更新,但其子节点有更新,会采用clone节点或者复用current已有的alternate节点,修改属性后使用;

    3. 对于有更新的节点,生成ReactElement(ClassComponent fibe 调用实例的render函数,FunctionComponent fibe 直接调用其elementType记录的生成函数),与current 树上的子fibe节点对比,若存在相同的key和type的(其实是fibe elementType与ReactElement type 对比),则clone修改属性后使用,若没有,则根据ReactElement 重新生成子节点,原current上对应的子节点树全部删除。(这就是Diffing算法的概要,详见下面的调和算法)

  • 对于有更新的fibe节点标记effectTag 。

对于有更新的fibe节点,在搭建时,会记录相应的effectTag。

如:对于ClassComponent类型的fibe组件,在搭建其子节点之前,会判断是否有有生命周期钩子函数需要在commit阶段执行,若定义了componentDidUpdate钩子函数,标记“update” tag; 若定义了getSnapshotBeforeUpdate钩子函数,则标记“Snapshot” tag;

搭建子节点时,对于新增和移动位置的fibe节点,会标记“palcement”tag,对于current上需要的删除的fibe,会标记“deletion”tag

  • 对于有更新的ClassComponent类型 fibe节点,同时也会执行需要在render阶段执行的钩子函数,包括:

    [UNSAFE_]componentWillMount (deprecated)
    [UNSAFE_]componentWillReceiveProps (deprecated)
    getDerivedStateFromProps
    shouldComponentUpdate
    [UNSAFE_]componentWillUpdate (deprecated)
    render
    

递归边界条件

递归前进段到达叶子节点,即返回。

每一次递归前进都会计算下一次递归单元,计算规则如下:

  1. 若当前节点存在子节点,则返回第一个子节点的地址,继续向下递归。

  2. 若已到叶子节点,不存在子节点,进入递归返回段。首先完成当前子节点在返回段的任务,再返回下一个兄弟节点进入递归;若不存在兄弟节点,继续完成父节点在返回段任务,再返回父节点的下一个兄弟节点进入递归,若父节点不存在下一个兄弟节点,则继续完成祖父节点返回段任务,返回祖父节点的下一个兄弟节点进入递归,以此类推。

  3. 当返回到hostRoot节点时,结束递归流程。

递归返回段

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

Commit阶段(CompleteRoot)

这个阶段相比第一个阶段,任务很轻,就是遍历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提高性能的另一个策略:异步调度。请看下回分解。

React的调和过程(Reconciliation)相关推荐

  1. 面试官:说说react的渲染过程

    面试官:说说react的渲染过程 hello,这里是潇晨,大家在面试的过程中有没有遇到过一些和react相关的问题呢,比如面试官让你说说react渲染的过程,这到题目比较开放,也比较考验大家对reac ...

  2. 源码解析 React Hook 构建过程

    2018 年的 React Conf 上 Dan Abramov 正式对外介绍了React Hook,这是一种让函数组件支持状态和其他 React 特性的全新方式,并被官方解读为这是下一个 5 年 R ...

  3. 记录一次react项目配置过程

    1.为什么要配置react而不是脚手架 因为要知其然,最好还要知其所以然! 2.配置对象 webpack webpack-dev-server babel eslint 3.配置过程 1.webpac ...

  4. 记一次node+react项目发布过程(一)--webpack生产环境打包优化

    先附上项目效果: 项目地址: http://47.105.144.204/index github: github.com/dsying/reac- 未优化之前 webpack配置文件 const p ...

  5. React 的诞生过程

    01 字符拼接时代 - 2004 时间回到 2004 年,Mark Zuckerberg 当时还在宿舍捣鼓最初版的 Facebook . 这一年,大家都在用 PHP 的字符串拼接(String Con ...

  6. react循环key值_React性能优化的几个知识点

    各位同学大家晚上好,今天来说说react相关的东西.<从零玩转React全家桶核心(21)>正在更新,视频版请登录官网(www.it666.com)查看,或者扫码直达: Diff算法 开发 ...

  7. 关于React的一切(updating...)

    1.React前言 前端UI的本质问题是如何将源于服务器端的动态数据和用户的交互行为高效的反映到复杂的用户界面上去.React另辟蹊径,通过引入虚拟DOM,状态,单项数据流等设计理念,形成以组件为核心 ...

  8. 前端面试 React篇(上)

    一.组件基础 1. React 事件机制 <div onClick={this.handleClick.bind(this)}>点我</div> React并不是将click事 ...

  9. react 面试题 高级_常见 React 面试题

    (给前端大全加星标,提升前端技能) 作者:小胡 https://github.com/nanhupatar/FEGuide/blob/master/框架/react.md React 中 keys 的 ...

  10. react 面试题 高级_常见react面试题汇总(适合中级前端)

    转载自<原文> React 中 keys 的作用是什么? Keys 是 React 用于追踪哪些列表中元素被修改.被添加或者被移除的辅助标识. render () {return( {th ...

最新文章

  1. Redis 的持久化方案
  2. linux centos 7安装 apache php 及mariadb
  3. bat java 指定堆大小_jvm 堆内存 栈内存 大小设置 查看堆大小
  4. 剑网服务器维护,12月31日服务器例行维护公告
  5. 数据结构之图的存储结构:邻接表法
  6. 〖Linux〗Ubuntu设定Proxy及忽略Proxy
  7. 开课吧Java课堂:如何通过接口引用实现接口?
  8. 干货 | 鸟瞰 MySQL,唬住面试官!
  9. iOS开发模式MVVM 2分离业务逻辑
  10. 《Java多线程编程核心技术》学习笔记(1)
  11. 运放输入偏置电流方向_运放中输入偏置电流和输入失调电流的区别??
  12. Eigen教程3----矩阵、向量以及标量的运算,转置、共轭以及伴随矩阵
  13. 【集合论】集合运算 ( 并集 | 交集 | 不相交 | 相对补集 | 对称差 | 绝对补集 | 广义并集 | 广义交集 | 集合运算优先级 )
  14. 微软因果推理的框架DoWhy github 介绍
  15. openssl升级解决系统安全漏洞问题
  16. C语言自定义类型——枚举类型讲解
  17. 使用editor编辑器遇到的小问题:editor.md工具栏置顶
  18. 华为云弹性文件服务 SFS
  19. 波束和BSS问题中的gevd
  20. 一位卖家对淘宝查杀虚假交易痛讼!

热门文章

  1. IDEA添加项目启动配置
  2. Excel文件加密后忘记密码 - 破解方法
  3. 《#华为云#听从你心,无问西东》及网友跟帖
  4. matlab 更换坐标轴_matlab导入数据生成曲线,并更改坐标轴刻度
  5. TensorFlow之saved_model使用笔记
  6. 微信支付之商户号以及appid以及密钥
  7. EBS采购订单创建发票
  8. Mac 安装非信任开发者软件
  9. html 英文自动换行,CSS解决英文自动换行有关问题
  10. 物联网安全硬件修改系列-硬改