React Fiber

175 阅读8分钟

React16之前的不足

由于React16之前UI渲染是同步进行的,致使组件树的递归遍历会长时间占用主线程,而在占用的这段时间内渲染线程是无法更新页面的,这就会导致主线程上的布局、动画等周期任务无法得到处理,最终出现视觉上的卡顿

浏览器在一帧中的工作

    • 处理用户的输入事件
    • JS执行
    • requestAnimation调用
    • 布局Layout
    • 绘制Paint

由此可见,当前帧的同步js执行时间过长会导致浏览器无法处理当前帧的其他工作

卡顿解决方案

使用合作式调度将渲染更新过程拆分为多个子任务,每次只做一小部分,做完看是否还有剩余的时间,如果有则处理下一个任务反之则挂起当前任务并将时间控制权交给主线程,等主线程不忙的时候继续执行

但是该方案在具体实现时会遇到如下问题

    • 如何拆分子任务
    • 一个子任务多大合适
    • 怎么判断是否有剩余时间
    • 有剩余时间应该执行什么任务

Fiber架构就是为了解决这些问题而产生的

React的Fiber架构

什么是Fiber

fiber的出现就是为了解决react逻辑执行时间过长导致的卡顿问题,其将用户的可中断任务分解为单元,并通过优先级自由调度子任务实现分段更新,从而将之前的同步渲染变为异步渲染(由之前的递归遍历组件转换为循环遍历组件)

这类似于一种契约调度,浏览器给程序一个执行时间片,程序需要在这个时间片内将任务执行完成并返还控制权给浏览器等待下一次的时间分配

数据结构的演进

Stack-Reconcilation

在react16之前使用的是该调和器,该调和器中DOM的更新是同步的并通过递归的方式渲染,如果发现了一个或多个instance有更新就会立即执行DOM操作

Fiber-Reconcilation

其为react16后更新的调和器,它允许进程渲染分段完成,中间可以返回主进程执行其他任务这一切的实现是在代码层引入了一个新的数据结构-Fiber对象,每一个组件实例都对应拥有一个fiber实例,一个fiber实例就是一个js对象,其属性可划分为以下部分

    • 节点类型信息:tag表示节点的分类,type表示具体的类型
    • 结构信息:单链表树结构,表示其在树中的定位
    • 相关数据:节点的组件实例、props、state等
    • Effect相关:副作用会保存在effectTag中

这些fiber实例拓展出了链表结构的fiber tree(既Fiber上下文的vDOM tree),正因为其使用了链表结构,即使处理流程被中断了,恢复之后仍然可以从上次未处理完的fiber继续循环遍历下去

// 单链表树结构
{
   return: Fiber | null, // 指向父节点
   child: Fiber | null,// 指向自己的第一个子节点
   sibling: Fiber | null,// 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
}

Fiber的工作流程

    • 第一部分从用户操作引起setState被调用后把接收的ReactElement转换为Fiber节点,并为其设置优先级、创建update,最后根据Fiber的优先级加入到Update相应的位置,该阶段主要进行的是初始数据的准备

    • 第二部分主要是三个函数 : scheduleWork 调度工作、requestWork 申请工作、performWork 正式工作,完成异步调度主要靠 scheduleCallbackWithExpriation 这个方法(注:该方法在不同浏览器环境下名称不同,chrome浏览器中名称为 requestIdleCallback API),任务调度的过程为:

      • 在任务队列中选出高优先级的fiber node执行
      • 调用requestIdleCallback获取所剩的时间,若执行时间超过deadline或者突然插入更高优先级的任务则执行中断并保存当前结果最后将tag设置为为pending
      • 等主线程释放出来后再继续执行之前pending的任务
    • 第三部分就是Fiber Reconciler,其分为两个阶段

      • 第一阶段为协调阶段,该阶段会遍历所有的Fiber节点并通过diff算法计算所有更新工作,最后产出EffectList供提交阶段使用
      • 第二阶段为提交阶段,该阶段处理EffectList,不可被打断

任务分片优先级

任务优先级有六种

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
module.exports = {
    synchronous,//0,synchronous首屏(首次渲染)用,要求尽量快,不管会不会阻塞UI线程
    task,//1,在next tick之前执行
    animation,//2,animation通过requestAnimationFrame来调度,这样在下一帧就能立即开始动画过程                                                                                                                                                                               
    high,//3,在不久的将来立即执行
    low,//4,稍微延迟执行也没关系
    offscreen,//5,下一次render时或scroll时才执行
}

Reconciliation Phase协调阶段

协调阶段以fiber tree为蓝本,把每个fiber作为一个工作单元,自顶向下逐节点构造workInProgress tree,workLoop过程如下

    • 找到高优先级的待处理节点
    • 判断当前节点是否需要更新,如果需要就打上tag标记并更新当前节点状态;如果不需要更新则直接将子节点clone过来并跳到render步骤
    • 如果该节点是组件节点则调用 shouldComponentUpdate 并判断其返回值,若为false则跳转至render步骤
    • 调用render获取新的子节点,生成子节点的workInProgress节点(创建过程中由alternate则使用alternate,没有的则复用子节点,子节点的增删也发生在该步)
    • 如果没有产生child fiber则该工作单元结束,将EffectList归并到父节点并将当前节点的兄弟节点作为下一个工作单元,否则将该节点的子节点左右工作单元
    • 如果没有剩余的可执行时间则等到下一次主线程空闲时开始下一个工作单元,否则立即开始工作单元
    • 如果没有下一个工作单元(回到workInProgress tree根节点)则第一阶段结束,进入pendingCommit阶段

工作循环就是从上至下执行beginWork函数,从下至上执行completeWork函数,执行完成后生成workInProgress树(即将渲染到界面中)

    • beginWork

      • 创建/更新(复用)子fiber节点(创建/diff)
      • 返回子fiber,一个个fiber构成workInProgress树
    • completeWork

      • 构建或更新DOM节点
      • 构建过程中生成DOM节点,将子孙DOM节点插入其中,处理props
      • 更新过程中处理更新的props与DOM节点,会为DOM几点对应的workinprogress中的fiber节点创建update的effectTag
      • 自上而下收集effectlist,最终将游离的Dom整合到root上(completeUnitOfWork)

commit 提交阶段

提交阶段可以理解为将Diff结果反映到真实DOM的过程,到这个阶段react拥有current与workInProgress两棵树和副作用表,其拥有三个阶段

    • before mutation:执行dom操作前 遍历effectlist 调用getSnapshotBeforUpdate生命周期 设置useEffect回调函数
    • mutation:执行dom操作 遍历effectlist 重置文字节点 DOM增删改 更新Ref
    • layout:执行dom操作后 调用生命周期 更新ref

双缓存

React拥有workInProgress tree与current两颗树,其分别对应着刷新到屏幕的未来状态与现有状态,在commit阶段React直接将current的指针指向workInProgress Tree而后丢掉旧的Fiber tree,这样做的好处有

    • 能够复用内部对象,节省内存分配
    • 就算运行中有错误也不影响view上的数据,避免了整棵树挂掉
    • 减少白屏时间

requestAnimationFrame 与 requestIdelCallback

requestAnimationFrame

requestAnimationFrame 是浏览器用于定时循环操作的一个接口,类似于 setTimeout,主要用途是按帧对网页进行重绘

其基本思想是与显示器的固有帧率保持同步,利用这个刷新频率对页面进行重绘,一旦用户离开,其会自动停止刷新,优化了性能

requestID = window.requestAnimationFrame(callback); // 传入一个回调函数作为参数,该回调函数会在浏览器重绘前调用

requestIdelCallback

requestIdelCallback 方法会在每次屏幕刷新时,判断当前帧是否还有多余的时间,如果有,则会调用requestAnimationFrame的回调函数

requestIdleCallback中的回调函数仅会在每次屏幕刷新并且有空闲时间时才会被调用,利用这个特性,我们可以在动画执行的期间,利用每帧的空闲时间来进行数据发送的操作,或者一些优先级比较低的操作,此时不会使影响到动画的性能,或者和requestAnimationFrame 搭配,可以实现一些页面性能方面的的优化,

react 的 fiber 架构也是基于 requestIdleCallback 实现的, 并且在不支持的浏览器中提供了 polyfill