看了React源码之后置信大家都会对Fiber有本人不同的见解,而我对Fiber最大的见解就是这玩意儿就是个链表。如果把整个Fiber树当成一个整体的确有点难了解源码,然而如果把它拆开了,将每个节点都看成一个独立单元却能失去一个很清晰的思路,接下来我就简略几点讲讲,我所认为的为什么React要用链表这种数据结构来构建Fiber架构
什么是Fiber
可能理解过React的靓仔就要说了,Fiber就是一个虚构dom树;的确如此,然而16版本之前的React也存在虚构dom树,为什么要用Fiber代替呢?
家喻户晓(可能有靓仔不晓得),16.8之前React还没引入Fiber概念,Reconciler(协调器)
会在mount阶段与update阶段循环递归mountComponent与updateComponent,此时数据存储在调用栈当中,因为是递归执行,所以一当开始便无奈进行直到递归执行完结;如果此时页面中的节点十分多咱们要等到递归完结可能要消耗大量的工夫,而且在此之间用户会感觉卡顿,这对用户来说相对称不上是好的体验;
因而在16版本之后React有了异步可中断更新与双缓存的概念,也就是咱们熟知的同步并发模式Concurrent模式,那么这些跟Fiber有什么关系呢?
首先咱们来看一段对于Fiber节点的React源码
function FiberNode(tag, pendingProps, key, mode) { // Instance //动态属性 this.tag = tag;// this.key = key; this.elementType = null;// this.type = null;//类型 this.stateNode = null; // Fiber //关联属性 this.return = null; this.child = null; this.sibling = null this.index = 0; this.ref = null; //工作属性 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; this.alternate = null; { // 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; } { // 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); } }}
能够看到在一个FiberNode当中存在很多属性,咱们大体将他们分为三类:
- 动态属性:保留以后Fiber节点的 标签,类型等;
- 关联属性:用于连贯其余Fiber节点造成Fiber树;
- 工作属性:保留以后Fiber节点的动静工作单元;
而多个Fiber节点之间正是通过关联属性的连贯造成一个Fiber树;因为每一个Fiber节点都是互相独立的,因而Fiber节点之间通过指针指向的形式产生分割,return指向的是父级节点,child指向的是子节点,sibling指向的是兄弟节点;
如下列这段JSX代码为例
<div className="App"> <div className='div1'> <div className='div2'> </div> </div> <div className='div3'> </div></div>
最终该JSX产生的树结构为
Fiber树的每个节点都是互相独立的,利用指针指向让他们关联在一起;那么咱们是不是能够说Fiber树就是一个链表,对于什么是链表,能够参考我这篇博文 《作为前端你是否理解链表这种数据结构?》
Fiber树是链表
可能当初就有靓仔要问了,为什么React要选用链表这种数据结构搭建Fiber架构?
我是这么思考的
- 节点独立
- 节俭操作工夫
- 利于双缓存与异步可中断更新操作
节点独立
不晓得有没有靓仔会说React的Fiber架构拿父节点的child存子节点拿子节点的return存父节点怎么就节点独立了呢?这位靓仔贫道倡议你再去学一下个别类型和援用类型;父节的child存的是子节点的内存地址,子节点的return存的是父节点的内存地址,因而并不会占用太多空间,说白了他们只是有一层关系将节点绑定在一起,然而这层关系并不是蕴含关系;就比方你女朋友是你女朋友,你是你一样,你们是情侣关系,并不是占有关系(不提倡啊!自由恋爱,人格独立);
节俭操作工夫与单向操作
如果Fiber树并不是链表这种数据结构而是数组这种数据结构会怎么样呢?咱们都晓得数组的存储须要在内存中开拓一长串有序的内存,如果我把两头的某个元素删除,那么前面的所有元素都要向上挪动一个存储空间,如果当初我有1000个节点,我把第一个节点删了,那么前面的999个节点都须要在内存空间上向上挪动一位,这显然是十分耗费工夫的;然而如果是链表的话咱们只须要将指针解绑,挪动到上一位节点或者下一节点就能造成一个新的链表,这在工夫上来说是十分有劣势的;因为是
节点间互相独立因而咱们仅仅只须要对指针进行操作并且它的操作是单向的咱们不须要进行双向解绑;
咱们持续以这段JSX为例
<div className="App"> <div className='div1'> <div className='div2'> </div> </div> <div className='div3'> </div></div>
如果此时咱们要将class为div1的节点删除fiber是如何操作的?咱们用图来解释
由图所示,咱们只须要将App的child指针改为div2,将div2的return指针改为App即可,而后咱们便能够对div1与div3进行销毁;
利于双缓存与异步可中断更新操作
异步可中断更新
我只能说React为了给用户良好的应用感触的确是下足了功夫,在React16之前React还采取着原始的同步更新,然而在在16之后React推出了concurrent模式也就是同步并发模式,在concurrent模式下你的mount与update都将成为异步可中断更新,至于react为什么要推出异步可中断更新可参考我这篇文章 《重学React之为什么须要Scheduler》
当初咱们用最直观的浏览器反馈来看一下Concurrent模式与Legacy模式的区别
咱们看看Legacy模式下的Performance的监听
能够看到所有的render阶段办法都在同一个Task实现,如果运行工夫过长将会造成卡顿;
咱们再看Concurrent模式下的Performance的监听
在concurrent模式下会react的render阶段会被分为若干个时长为5ms的Task
这所有归功于Scheduler调度器的功绩,因为16之前的React没有Scheduler所以采纳的是所以采纳的是递归的形式将数据存储在调用栈当中,递归一旦开始便无奈进行,所以起初有了Scheduler;而采纳链表这种数据结构(Fiber)存储数据却能很好的中断遍历;咱们来看看Concurrent模式下的入口函数
function workLoopConcurrent() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); }}
能够看到当shouldYield() 为true时workLoopConcurrent办法将会中断工作,而shouldYield() 对应的正是scheduler是否须要更新调度的状态
双缓存
双缓存的概念在座的靓仔应该都分明,React在运行时会有两棵Fiber树 (mount阶段只有workInProgress Fiber树),
一颗是current Fiber树,对应以后展现的内容,一颗是workInProgress Fiber树对应的是正在构建的Fiber树,在mount阶段的首次创立会创立一个fiberRootNode的根节点,fiberRootNode 有一个current工作单元属性,来回指向Fiber树,当workInProgess Fiber树构建实现之后current就指向workInprogress Fiber树,此时workInProgess Fiber树变为current Fiber树,而current Fiber树将变为workInProgess Fiber树,因为这一切都是在内存中进行的,所以称之为双缓存;
而这所有刚好使用了链表的灵便指向,一直造成一个新的链表;
总结
没什么总结~~能叫我一声靓仔吗?