React 调和(Reconciler)原理理解

Posted YuLong~W

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React 调和(Reconciler)原理理解相关的知识,希望对你有一定的参考价值。

文章目录

Fiber

Reactv15及之前,React 对于虚拟 DOM 是采用 递归方式 遍历更新的,一次更新,从应用根部递归更新,递归开始后中途无法终端,随着项目复杂,层级变深,导致更新时间变成,给前端交互上的体验就卡顿。

Fiber 诞生在 Reactv16 版本,Fiber 架构目的就是解决大型 React 应用卡顿,fiber 在 React 中是最小粒度的执行单元,在遍历更新每一个节点的时候都不是用的真实 DOM ,都是采用虚拟 DOM ,所以可以 理解成 fiber 就是 React 的虚拟 DOM

Fiber解决:

  • 更新 fiber 的过程叫做 Reconciler(调和器),每一个 fiber 可以作为一个 执行单元 来处理,所以每一个 fiber 可以根据自身的过期时间 expirationTime( v17 版本叫做优先级 lane )来判断是否还有空间时间执行更新。
  • 如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘(repaints) 等。
  • 等浏览器空余时间,再通过 Scheduler (调度器),再次恢复执行单元。在本质上中断了渲染,提升用户体验。

element、fiber、dom 三者间关系:

  • element 是 React 视图层在代码层级上的表象,也就是开发者写的 jsx 语法,写的元素结构,都会被创建成 element 对象的形式。上面保存了 props , children 等信息。
  • dom 是元素在浏览器上给用户直观的表象。
  • fiber 可以理解为 element 和真实 dom 之间的交流枢纽站,每一个类型 element 都会有一个与之对应的 fiber 类型,element 变化引起更新流程都是通过 fiber 层面做一次调和改变,然后对于元素,形成新的 dom 做视图渲染。

element 与 fiber 之间的对应关系:

fiberelement
FunctionComponent = 0函数组件
ClassComponent = 1类组件
IndeterminateComponent = 2初始化的时候不知道是函数组件还是类组件
HostRoot = 3Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
HostPortal = 4ReactDOM.createPortal 产生的 Portal
HostComponent = 5dom 元素 比如
HostText = 6文本节点
Fragment = 7<React.Fragment>
Mode = 8<React.StrictMode>
ContextConsumer = 9<Context.Consumer>
ContextProvider = 10<Context.Provider>
`ForwardRef = 11React.ForwardRef
Profiler = 12<Profiler>
SuspenseComponent = 13<Suspense>
MemoComponent = 14React.memo 返回的组件

fiber 保存信息:

function FiberNode()

  this.tag = tag;                  // fiber 标签 证明是什么类型fiber。
  this.key = key;                  // key调和子节点时候用到。 
  this.type = null;                // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。  
  this.stateNode = null;           // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。
 
  this.return = null;              // 指向父级fiber
  this.child = null;               // 指向子级fiber
  this.sibling = null;             // 指向兄弟fiber 
  this.index = 0;                  // 索引

  this.ref = null;                 // ref指向,ref函数,或者ref对象。

  this.pendingProps = pendingProps;// 在一次更新中,代表element创建
  this.memoizedProps = null;       // 记录上一次更新完毕后的props
  this.updateQueue = null;         // 类组件存放setState更新队列,函数组件存放
  this.memoizedState = null;       // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
  this.dependencies = null;        // context或是时间的依赖项

  this.mode = mode;                //描述fiber树的模式,比如 ConcurrentMode 模式

  this.effectTag = NoEffect;       // effect标签,用于收集effectList
  this.nextEffect = null;          // 指向下一个effect

  this.firstEffect = null;         // 第一个effect
  this.lastEffect = null;          // 最后一个effect

  this.expirationTime = NoWork;    // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。

  this.alternate = null;           //双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。

fiber建立起关联:

每一个 element 都会对应一个 fiber ,每一个 fiber 是通过 return , child ,sibling 三个属性建立起联系的。

  • return: 指向父级 Fiber 节点。
  • child: 指向子 Fiber 节点。
  • sibling:指向兄弟 fiber 节点。

Fiber 更新机制

1、初始化

第一步:创建 fiberRoot 和 rootFiber

  • fiberRoot:首次构建应用, 创建一个 fiberRoot ,作为整个 React 应用的根基。
  • rootFiber: 通过 ReactDOM.render 渲染出来的。

注意:一个 React 应用可以有多 ReactDOM.render 创建的 rootFiber ,但是只能有一个 fiberRoot(应用根节点)

第二步:workInProgress和current

开始到正式渲染阶段,会进入 beginwork 流程

  • workInProgress:正在内存中构建的 Fiber 树称为 workInProgress Fiber 树。在一次更新中,所有的更新都是发生在 workInProgress 树上。在一次更新之后,workInProgress 树上的状态是最新的状态,那么它将变成 current 树用于渲染视图。
  • current:正在视图层渲染的树叫做 current Fiber树

rootFiber 的渲染流程: 首先会复用当前 current 树( rootFiber )的 alternate 作为 workInProgress 。如果没有 alternate (初始化的 rootFiber 是没有 alternate ),那么会创建一个 fiber 作为 workInProgress会用 alternate 将新创建的 workInProgress 与 current 树建立起关联

currentFiber.alternate = workInProgressFiber

workInProgressFiber.alternate = currentFiber


第三步:深度调和子节点,渲染视图

在新创建的 alternates 上,完成整个 fiber 树的遍历,包括 fiber 的创建。

最后会以 workInProgress 作为最新的渲染树,fiberRoot 的 current 指针指向 workInProgress 使其变为 current Fiber 树。到此完成初始化流程。


2、更新

首先会重新创建 workInProgresss 树,复用当前 current 树上的 alternate ,作为新的 workInProgress

由于初始化 rootfiber 有 alternate ,所以对于剩余的子节点,React 还需要创建一份,和 current 树上的 fiber 建立起 alternate 关联。

渲染完毕后,workInProgresss 再次变成 current 树。

总结:

① 每一个 fiber 可以看作一个执行的单元。

② 在调和过程中,每一个发生更新的 fiber 都会作为一次 workInProgress 。

双缓冲树

问题:canvas 绘制动画的时候,如果上一帧计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

解决: canvas 在内存中绘制当前动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

这种在内存中构建并直接替换的技术叫做 双缓存

React 用 workInProgress 树(内存中构建的树)current (渲染树) 来实现更新逻辑。双缓存一个 在内存中构建,一个 渲染视图,两颗树用 alternate 指针相互指向,在下一次渲染的时候,直接复用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树,这样可以 防止只用一棵树更新状态的丢失的情况,又加快了 DOM 节点的替换与更新。

render阶段

1、workLoop

fiber 的遍历开始—— workLoop,workLoop 就是执行每一个单元的 调度器,如果渲染没有被中断,那么 workLoop 会遍历一遍 fiber 树。


2、performUnitOfWork

performUnitOfWork 包括两个阶段 beginWorkcompleteWork

  • beginWork:是 向下调和 的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点,打不同 effectTag
  • completeUnitOfWork:是 向上归并 的过程。如果有兄弟节点,会返回 sibling兄弟,没有返回 return 父级,一直返回到 fiebrRoot ,期间可以形成 effectList,对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理style,className等。

3、beiginWork


beiginWork 作用:

  • 对于组件,执行部分生命周期,执行 render ,得到最新的 children 。
  • 向下遍历调和 children ,复用 oldFiber ( diff 算法)
  • 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新。

常用的 effectTag:

effectTag含义
Placement =0b0000000000010插入节点
Update = 0b0000000000100更新fiber
Deletion = 0b0000000001000删除fiebr
Snapshot = 0b0000100000000快照
Passive =0b0001000000000useEffect的副作用
Callback =0b0000000100000setState的 callback
Ref = 0b0000010000000ref

4、completeUnitOfWork

  • completeUnitOfWork 会将 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中。在 commit 阶段,将不再需要遍历每一个 fiber ,只需要执行更新 effectList 。
  • 对于组件处理 context ;对于元素标签初始化,会创建真实 DOM ,将子孙 DOM 节点插入刚生成的 DOM 节点中;会触发 diffProperties 处理 props ,比如事件收集,style,className 处理。

commit 阶段

  • 一方面是 对一些生命周期和副作用钩子的处理,比如 componentDidMount ,函数组件的 useEffect ,useLayoutEffect ;
  • 另一方面就是 在一次更新中,添加节点( Placement ),更新节点( Update ),删除节点( Deletion ),还有就是 一些细节的处理,比如 ref 的处理。

commit 可以分为:

  • Before mutation 阶段(执行 DOM 操作前);
  • mutation 阶段(执行 DOM 操作);
  • layout 阶段(执行 DOM 操作后)

1、Before mutation

  • 还没修改真实的 DOM ,是 获取 DOM 快照的最佳时期,如果是类组件有 getSnapshotBeforeUpdate ,那么会执行这个生命周期。
  • 异步调用 useEffect , useEffect 是采用异步调用的模式,其目的就是 防止同步执行时阻塞浏览器做视图渲染。

2、Mutation

  • 置空 ref ,对于 ref 的处理。
  • 对新增元素,更新元素,删除元素。进行真实的 DOM 操作

3、Layout

  • commitLayoutEffectOnFiber 对于类组件,会执行生命周期setState 的callback,对于函数组件会执行 useLayoutEffect 钩子
  • 如果有 ref ,会 重新赋值 ref

调和+异步调度 原理图

总结

1、Fiber组成

2、Fiber 更新机制(初始化和更新)、双缓冲树

3、Fiber 调和过程(render和commit阶段)

以上是关于React 调和(Reconciler)原理理解的主要内容,如果未能解决你的问题,请参考以下文章

React 调和(Reconciler)原理理解

第十篇:React 中的“栈调和”(Stack Reconciler)过程是怎样的?

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber