什么是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节点的TagNamethis.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;// Effectsthis.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" />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的变化