六个问题让你更懂 React Fiber(中)

简介: 接上文。

五、 Fiber 为什么是 React 性能的一个飞跃?


什么是 Fiber


Fiber 的英文含义是“纤维”,它是比线程(Thread)更细的线,比线程(Thread)控制得更精密的执行模型。在广义计算机科学概念中,Fiber 又是一种协作的(Cooperative)编程模型(协程),帮助开发者用一种【既模块化又协作化】的方式来编排代码。


在 React 中,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,避免了之前采用递归需要一气呵成影响性能的做法


React Fiber 中的时间分片


把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。


React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。


Stack Reconciler


基于栈的 Reconciler,浏览器引擎会从执行栈的顶端开始执行,执行完毕就弹出当前执行上下文,开始执行下一个函数,直到执行栈被清空才会停止。然后将执行权交还给浏览器。由于 React 将页面视图视作一个个函数执行的结果。每一个页面往往由多个视图组成,这就意味着多个函数的调用。


如果一个页面足够复杂,形成的函数调用栈就会很深。每一次更新,执行栈需要一次性执行完成,中途不能干其他的事儿,只能"一心一意"。结合前面提到的浏览器刷新率,JS 一直执行,浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个时间超过 16ms,当页面有动画效果需求时,动画因为浏览器不能及时绘制下一帧,这时动画就会出现卡顿。不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会延迟。


Fiber Reconciler


链表结构


在 React Fiber 中用链表遍历的方式替代了 React 16 之前的栈递归方案。在 React 16 中使用了大量的链表。


  • 使用多向链表的形式替代了原来的树结构;


<div id="A">
  A1
  <div id="B1">
    B1
    <div id="C1"></div>
  </div>
  <div id="B2">
    B2
  </div>
  </div>


f2321e5740a2f7cdb26598a21f5780a7.png


  • 副作用单链表;


230fad9accae9d46efba7c639c239ed9.png


  • 状态更新单链表;


40d9e36739931be2579f6b5efaec1cd3.png


  • ...


链表是一种简单高效的数据结构,它在当前节点中保存着指向下一个节点的指针;遍历的时候,通过操作指针找到下一个元素。


59b95d712eeb12a5597f28213017f3e4.png

链表相比顺序结构数据格式的好处就是:


  1. 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了。


  1. 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到他的父节点或者兄弟节点。


但链表也不是完美的,缺点就是:


  1. 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。


  1. 不能自由读取,必须找到他的上一个节点。


React 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在下面提到的挂起和恢复过程中起到了关键作用


斐波那契数列的 Fiber


递归形式的斐波那契数列写法:


function fib(n) {
  if (n <= 2) {
    return 1;
  } else {
    return fib(n - 1) + fib(n - 2);
  }
}


采用 Fiber 的思路将其改写为循环(这个例子并不能和 React Fiber 的对等):


function fib(n) {
  let fiber = { arg: n, returnAddr: null, a: 0 }, consoled = false;
  // 标记循环
  rec: while (true) {
    // 当展开完全后,开始计算
    if (fiber.arg <= 2) {
      let sum = 1;
      // 寻找父级
      while (fiber.returnAddr) {
        if(!consoled) {
          // 在这里打印查看形成的链表形式的 fiber 对象
          consoled=true
          console.log(fiber)
        }
        fiber = fiber.returnAddr;
        if (fiber.a === 0) {
          fiber.a = sum;
          fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
          continue rec;
        }
        sum += fiber.a;
      }
      return sum;
    } else {
      // 先展开
      fiber = { arg: fiber.arg - 1, returnAddr: fiber, a: 0 };
    }
  }
}


六、React Fiber 是如何实现更新过程可控?


更新过程的可控主要体现在下面几个方面:


  • 任务拆分


  • 任务挂起、恢复、终止


  • 任务具备优先级


任务拆分


在 React Fiber 机制中,它采用"化整为零"的思想,将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。


任务挂起、恢复、终止


workInProgress tree


workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链


currentFiber tree


currentFiber 表示上次渲染构建的 Filber 树在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。


在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。


挂起


当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。


恢复


在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。


  1. 如何判断一帧是否有空闲时间的呢?


使用前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。


  1. 恢复执行的时候又是如何知道下一个任务是什么呢?


答案是在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。


终止


其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。


82c7ba40a8baad70b9239d61f692b8ab.png


任务具备优先级


React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行该任务。过期时间的大小还代表着任务的优先级


任务在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表 A1(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。


其实最终都是为了收集到这条副作用链表,有了它,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的这个动作是一气呵成的,不能中断,不然会造成视觉上的不连贯(commit)。


<div id="A1">
  A1
  <div id="B1">
    B1
    <div id="C1">C1</div>
    <div id="C2">C2</div>
  </div>
  <div id="B2">
    B2
  </div>
</div>


48d2ea3ac7082f9285f56cfe3cf181f3.png


直观展示


正是基于以上这些过程,使用Fiber,我们就有了在社区经常看到的两张对比图[10]


清晰展示及交互、源码可通过下面两个链接进入,查看网页源代码。


  • Stack Example[11]


  • Fiber Example[12]


为了方便大家对比,我就直接放上两张对比图吧,大家自行比对,差别还是很明显的


45bd32163f1cba56be698686bb24634e.jpg


9b57ff7fc4b6d5ac7c044a873a86bfe6.jpg



相关文章
|
3月前
|
前端开发 JavaScript 调度
【第32期】一文学会用React Fiber提升性能
【第32期】一文学会用React Fiber提升性能
64 0
|
13天前
|
缓存 前端开发 JavaScript
理解 React 的 Fiber 架构
【8月更文挑战第6天】 理解 React 的 Fiber 架构
25 1
|
前端开发
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍2
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍2
42 0
|
3月前
|
移动开发 前端开发 JavaScript
react fiber架构【详细讲解,看这一篇就够了】
react fiber架构【详细讲解,看这一篇就够了】
398 0
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
55 0
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
|
前端开发
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍2
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍2
47 0
|
前端开发
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍1
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍1
56 0
|
前端开发
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍
60 0
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-构建多个子集fiber的情况
前端学习笔记202307学习笔记第六十天-react源码-构建多个子集fiber的情况
37 0
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-构建单个子集fiber的情况2
前端学习笔记202307学习笔记第六十天-react源码-构建单个子集fiber的情况2
37 0

热门文章

最新文章