关于javascript:React-Fiber架构不就是个链表么

42次阅读

共计 4755 个字符,预计需要花费 12 分钟才能阅读完成。

看了 React 源码之后置信大家都会对 Fiber 有本人不同的见解,而我对 Fiber 最大的见解就是这玩意儿就是个链表。如果把整个 Fiber 树 当成一个整体的确有点难了解源码,然而如果把它拆开了,将每个节点都看成一个独立单元却能失去一个很清晰的思路,接下来我就简略几点讲讲, 我所认为的为什么 React 要用链表这种数据结构来构建Fiber 架构

什么是 Fiber

可能理解过 React 的靓仔就要说了,Fiber就是一个虚构 dom 树;的确如此,然而 16 版本之前的 React 也存在虚构 dom 树,为什么要用 Fiber 代替呢?

家喻户晓 (可能有靓仔不晓得),16.8 之前React 还没引入 Fiber 概念,Reconciler(协调器)
会在 mount 阶段与 update 阶段循环递归 mountComponentupdateComponent,此时数据存储在调用栈当中,因为是递归执行,所以一当开始便无奈进行直到递归执行完结;如果此时页面中的节点十分多咱们要等到递归完结可能要消耗大量的工夫,而且在此之间用户会感觉卡顿,这对用户来说相对称不上是好的体验;

因而在 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 当中存在很多属性,咱们大体将他们分为三类:

  1. 动态属性:保留以后 Fiber 节点的 标签,类型等;
  2. 关联属性:用于连贯其余 Fiber 节点造成 Fiber 树;
  3. 工作属性:保留以后 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 架构

我是这么思考的

  1. 节点独立
  2. 节俭操作工夫
  3. 利于双缓存与异步可中断更新操作

节点独立

不晓得有没有靓仔会说 ReactFiber 架构 拿父节点的 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 模式下你的 mountupdate都将成为异步可中断更新,至于 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 树,因为这一切都是在内存中进行的,所以称之为双缓存;

而这所有刚好使用了链表的灵便指向,一直造成一个新的链表;

总结

没什么总结~~ 能叫我一声靓仔吗?

正文完
 0