React源码之Fiber架构

Posted 我的名字豌豆

tags:

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

什么是Fiber?

Fiber又命为纤程,他是与进程、线程、协程同为操作系统中程序的执行过程,可以将纤程理解为操作系统中的协程,那么generator也可以理解为协程,为什么没有使用generator来作为协程呢?

  • 当使用generator与async与await一样也是有传染性的,需要不断向上面声明*或者类似async与await中的async。
  • 实现更新可以中断并继续,实现更新可以拥有不同的优先级,高优先级的更新可以打断低优先级的更新,使用generator可以达到第一个目的,但是无法实现高优先级的更新可以打断低优先级的更新。

两种定义

静态数据结构

对于Fiber也作为一个静态数据结构,对于着一个组件保存了该组件的类型和对应的Dom节点等信息,这个时候的Fiber节点也就是我们所说的虚拟Dom。
在一个页面中可以有多个RootFiber,但是需要有一个FiberRootNode来管理这些RootFiber。

动态工作单元

每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除、被插入页面中、被更新…)。
对于Fiber我们可以理解为存储在内存中的Dom,在React15的协调器(Reconciler)是通过递归调用执行栈来实现的,在React16中的协调器(Reconciler)是基于Fiber节点来实现的。
对于React15在render阶段的reconcile是不可打断的,如果在操作大量的dom时,会存在卡顿,因为浏览器将所有的时间都交给了js引擎线程去执行,此时GUI渲染线程被阻塞,导致页面出现卡顿,无法响应用户对应的事件。
所以在React16之后就有了Scheduler来进行时间片的调度,给每一个task一定的时间,如果在这个时间内没有执行完,也要交出执行权给浏览器进行绘制和重排,所以异步可中断的更新需要一定的数据结构在内存中保存dom信息,所以产出了这样一种数据结构Fiber,也可以称为虚拟Dom。

Fiber数据结构

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
  ) 
    // Instance
    // tag是Fiber对应的组件类型,比如Funtion Component、Class Component、Hooks Component
    // 其中Hooks Component是指Dom节点对应的Fiber节点
    this.tag = tag;
    this.key = key;
    // elementType大部分情况与type相同,除非Function Component使用React.memo来包裹时,他的ElementType与type不同
    // type对于Funtion Component来说是函数本身,对于Class Component是Class,对于Hooks Component是Dom节点的TagName
    this.elementType = null;
    this.type = null;
    // stateNode对于Hooks Components来说指对应的真实Dom节点
    this.stateNode = null;
    
    // Fiber
    // return/child/sibling会链接储存一颗Fiber树
    this.return = null;
    this.child = null;
    this.sibling = null;
    // 对于多个同级的Fiber节点,代表插入Dom的位置索引
    this.index = 0;
    // 就是我们常用的Ref属性
    this.ref = null;
    
    // 下面的属性都是将Fiber作为动态的工作单元使用时的属性 
    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.updateQueue = null;
    this.memoizedState = null;
    this.dependencies = null;
    
    this.mode = mode;
    
    // Effects
    this.flags = NoFlags;
    this.subtreeFlags = NoFlags;
    this.deletions = null;
    
    // 调度优先级
    this.lanes = NoLanes;
    this.childLanes = NoLanes;
    // 关系到Fiber架构的工作方式
    this.alternate = null;
    
    if (enableProfilerTimer) 
      // Note: The following is done to avoid a v8 performance cliff.
      //
      // Initializing the fields below to smis and later updating them with
      // double values will cause Fibers to end up having separate shapes.
      // This behavior/bug has something to do with Object.preventExtension().
      // Fortunately this only impacts DEV builds.
      // Unfortunately it makes React unusably slow for some applications.
      // To work around this, initialize the fields below with doubles.
      //
      // Learn more about this here:
      // https://github.com/facebook/react/issues/14365
      // https://bugs.chromium.org/p/v8/issues/detail?id=8538
      this.actualDuration = Number.NaN;
      this.actualStartTime = Number.NaN;
      this.selfBaseDuration = Number.NaN;
      this.treeBaseDuration = Number.NaN;
    
      // It's okay to replace the initial doubles with smis after initialization.
      // This won't trigger the performance cliff mentioned above,
      // and it simplifies other profiler code (including DevTools).
      this.actualDuration = 0;
      this.actualStartTime = -1;
      this.selfBaseDuration = 0;
      this.treeBaseDuration = 0;
    
    
    if (__DEV__) 
      // This isn't directly used but is handy for debugging internals:
    
      this._debugSource = null;
      this._debugOwner = null;
      this._debugNeedsRemount = false;
      this._debugHookTypes = null;
      if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') 
        Object.preventExtensions(this);
      
    


Fiber双缓存

什么是双缓存呢?我们可以由动画来举例
动画由多张连续的图片组成,每一张图可以成为动画的一帧,当我们播放动画时需要有一定的速度连续展示每一帧,在渲染新的一帧的图片时需要将前一帧的图片清除,如果从清除前一帧图片到下一帧之间所展示的图片过长就会出现人眼能够感知的白屏闪烁,为了解决这个问题,我们可以从内存中绘制当前帧的图片,绘制完毕后直接用当前帧去替换掉上一帧的图片,由于这两帧替换出来所消耗的时间不会出现从白屏到画面闪烁的情况(速度足够快),这种在内存中构建并直接替换的技术称之为双缓存。
举例:
对于这样的Dom结构会生成如下的Fiber

function App() 
  return (
    <div>
      zi
      <p>bai</p>
    </div>
  )

ReactDOM.render(<App />, document.getElementById("root"));

对于上述结构的Fiber图如下:

mount

对于前面对于Fiber定义的讲解,我们知道了Fiber作为虚拟Dom可以保存和描述真实的dom,在mount的时候会先将jsx对象构建Fiber对象,形成Fiber树,这颗Fiber树叫做current Fiber并对应到真实Dom上。而正在构建的Fiber树叫做workInProgress Fiber,两棵树的节点会通过alternate相连。
对于首次进入页面的渲染时会通过ReactDom.Render来创建FiberRootNode,对于fiberRoot是指整个应用的根节点,只存在一个。每次调用ReactDom.Render都会创建当前应用的根节点RootFiber,对于rootFiber是指ReactDom.render或者ReactDOM.unstable_createRoot所创建出来的节点,可以存在多个其中会有一个current指针来指向RootFiber节点,由于在首屏渲染之前页面是空白的所以RootFiber没有子节点,接下来无论是首屏渲染还是调用this.setState或者调用useState的Hooks方法来创建的更新都会从根节点来开始创建一颗Fiber树。
在Mount时候只会创建FiberRootNode和rootFiber两个节点。初始时如下:

然后根据jsx创建workInProgress Fiber,然后通过alternate链接

接着workInProgress Fiber会和current Fiber交换位置,此时workInProgress变为current Fiber并渲染成对应的真实Dom则为如下图:

update

对于更新来讲,我们可以理解为动画帧,双缓存的替换类比为动画的两帧,当动画的下一帧替换动画的上一帧的速度够快就不会出现卡顿现象。此时我们来类比为Fiber,当更新时会在内存中生成workInProgress,然后将workInProgress替换为current Fiber并将current指针指向workInProgress Fiber并渲染为对应的Dom,如果执行速度够快就不会出现卡顿的现象。具体过程如下:
在update时会根据current Fiber与状态变更后的jsx对象做对比形成新的workInProgress Fiber,其过程也就是diff算法,然后workInProgress Fiber切换成current Fiber应用到真实dom就达到了更新的目的,这一切都是在内存中发生的,减少了对dom的操作。

最后再把workInProgress Fiber切换为current Fiber

上述为update时,current Fiber与workInProgress Fiber的变化

深入理解React16之:(一).Fiber架构

参考技术A

React16虽然出了一阵子了。刚出来的时候,粗略看了一遍更新文档。以为没什么大的改动,也听说项目从react15-16的升级过度可以很平滑,再加上项目改版上线一直比较频繁,所以一直还用的15.6的版本。
偶然在知乎看到@程墨Morgan大神的live,便抱着好奇心和学习的心态报名了,受益良多。

我理解的Fiber架构:

在我之前的一篇文章有简单介绍, 阅读react源码--记录:1.1 问题记录
下面从一个具体实例理解一下,再加上我画了图,应该很好理解啦~(图画的有点渣)

假如有A,B,C,D组件,层级结构为:

我们知道组件的生命周期为:
挂载阶段:

那么在挂载阶段,A,B,C,D的生命周期渲染顺序是如何的呢?

以render()函数为分界线。从顶层组件开始,一直往下,直至最底层子组件。然后再往上。

组件update阶段同理。

————————
前面是react16以前的组建渲染方式。这就存在一个问题,

好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,然后他再上岸。在这期间, 岸上发生的任何事,都不能对他进行干扰,如果有更重要的事情需要他去做(如用户操作),也必须得等他上岸

看一下fiber架构 组建的渲染顺序

潜水员会每隔一段时间就上岸,看是否有更重要的事情要做。

加入fiber的react将组件更新分为两个时期

这两个时期以render为分界,

phase1的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务。此过程,React 在 workingProgressTree (并不是真实的virtualDomTree)上复用 current 上的 Fiber 数据结构来一步地(通过requestIdleCallback)来构建新的 tree,标记处需要更新的节点,放入队列中。

phase2的生命周期是不可被打断的,React 将其所有的变更一次性更新到DOM上。

这里最重要的是phase1这是时期所做的事。因此我们需要具体了解phase1的机制。

这样的话,就和react16版本之前有很大区别了,因为可能会被执行多次,那么我们最好就得保证phase1的生命周期 每一次执行的结果都是一样的 ,否则就会有问题,因此, 最好都是纯函数。

对了,程墨大神还提到一个问题,饥饿问题,即如果高优先级的任务一直存在,那么低优先级的任务则永远无法进行,组件永远无法继续渲染。这个问题facebook目前好像还没解决,但以后会解决~

所以,facebook在react16增加fiber结构,其实并不是为了减少组件的渲染时间,事实上也并不会减少,最重要的是现在可以使得一些更高优先级的任务,如用户的操作能够优先执行,提高用户的体验,至少用户不会感觉到卡顿~

以上是关于React源码之Fiber架构的主要内容,如果未能解决你的问题,请参考以下文章

前端大佬谈 React Fiber 架构

React 之 简易实现 Fiber架构

react为何采用fiber架构

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber