理解 React Fiber 架构

303 阅读7分钟

一、React 架构

React15 架构可以分为两层:

  • Reconciler 协调:通过 diff 算法递归比较虚拟dom树,找出变化的组件。
  • Renderer 渲染:负责将变化的组件渲染到页面上。

React16 的架构分为三层,它新增了 Scheduler 阶段,并且重构了 Reconciler,即 Fiber Reconciler

  • Scheduler 调度:调度任务的优先级,高优先级任务会优先进入 Reconciler。
  • Reconciler 协调:负责找出变化的组件,更新工作变成了可以中断的,Reconciler 内部采用了 Fiber 的架构。
  • Renderer 渲染:负责将变化的组件渲染到页面上。

二、对 Fiber 的理解

1. Fiber 是什么

Fiber 可以理解成将一个大任务拆分成的一个个小的执行单元。每执行完一个 Fiber,React 就会检查是否还有空余时间,如果没有时间了就将控制权交还给浏览器,而不需要执行完整个任务才去响应用户。

image.png

2. 为什么 React16 要设计 fiber

  • 首先JS引擎和GUI渲染引擎在同一个线程上,两者是互斥关系。如果 js 执行的时间比较长,那么就会阻塞页面的渲染,出现卡顿的现象。

  • v16之前 react 通过对比虚拟DOM树找出需要变动的节点、并更新到页面上,这个协调过程(reconcilation是不能被打断的。所以在reconcilation期间 react 会一直占用浏览器资源,导致用户触发的事件得不到及时响应。

  • 为了解决以上问题,v16 设计了 Fiber 架构来让协调过程变成可被中断的。它将渲染更新过程拆分成一个个小块的任务,通过合理的调度机制去指定任务执行的时机、让出 CPU 执行权,从而提高浏览器的用户响应速率。

忽略内容:通常流畅动画的帧率是60HZ,1帧约16ms。

3. Fiber 的调度过程

  1. 优先级:每个更新任务都会设置一个优先级。当任务抵达调度器时,高优先级的更新任务会先进入到 Reconciler 层

  2. 可中断:当有更高优先级的更新任务抵达调度器时,Reconciler 层的任务就会被中断,然后调度器将更高级的任务推入 Reconciler 层。

  3. Reconciler 层 的详细过程为 三、Fiber Reconciler

可恢复:当前任务完成渲染后,新一轮的调度开始。之前被中断的任务将会被重新推入 Reconciler 层,继续渲染,即“可恢复”。

三、Fiber Reconciler

协调过程分两个阶段:

1. reconciliation/render 阶段

负责找出所有节点的改变,这个阶段是可中断的 (对应早期版本的 diff 过程)。构建workingInProgress的过程就是diff的过程。

  1. 以“虚拟dom节点”为维度对任务进行拆分,即一个虚拟dom节点对应一个任务。(如果当前节点不需要更新就直接拷贝,需要的话就打上tag

  2. 主线程空闲时会调用requestIdleCallback执行任务。从根节点开始遍历、通过 diff 算法构建 WIP 树,每处理完一个 fiber 会检查是否还有空闲时间,有的话继续处理下一个任务,没有时间就把控制权交还给主线程,直到下一次requestIdleCallback再继续构建 WIP 树。

  3. 最后产出Effect list,记录哪些节点更新、增加或删除了。

2. commit 阶段

负责执行所有的改变,这个阶段是不可中断的(对应早期版本的 patch 过程)。根据Effect list将所有更新都 commit 到DOM树上。

为什么commit不能中断,reconciliation可以中断

  • 协调阶段执行的任务不会导致任何用户可见的改变,因为WIP树是一份用户看不见的草稿,所以在这个阶段让出控制权不会有什么问题。

  • 而 commit 阶段会批量更新真实dom,用户看得见,所以不能暂停,否则会出现 UI 更新卡顿的现象。

补充了解:current 树/workInProgress 树+双缓冲

双缓冲技术是在内存中构建并直接替换的技术,例如 Fiber 树的构建与替换,其中 current 树和 workInProgress 树的节点通过alternate 属性相连。

  • current 树:当前在屏幕中显示的树。
  • workInProgress 树:即将显示在页面中的 Fiber 树。当它构建完成后,React 会将它与 current 树进行替换,从而快速更新 dom

首次 render 时,React 会创建一棵虚拟 dom 树,并且会为每个 React 元素创建一个 Fiber 节点,最终得到了 Fiber 树(current Tree,链表结构)。

setState 再次更新时,会根据 current 树和最新的虚拟 dom ,生成一个新的 Fiber 树(workInProgress Tree)。

(可忽略)在后续的更新过程中,每次重新渲染都会重新创建 Element,但是 Fiber 不会,它只会使用对应的 Element 中的数据来更新自己的属性。

四、 实现调度的两个浏览器API

1. requestIdleCallback:适用于低优先级

requestIdleCallback will schedule work when there is free time at the end of a frame, or when the user is inactive.

requestIdleCallback是 react fiber 实现的基础 api,它告诉浏览器在 “有空” 的时候就执行我们的回调。(控制权让出)

window.requestIdleCallback ( 
    callback: (dealine: IdleDeadline) => void, 
    option?: {timeout: number} 
)

interface IdleDealine { 
    didTimeout: boolean // 表示任务执行是否超时 
    timeRemaining(): DOMHighResTimeStamp // 当前帧还剩多少时间供任务执行 
}

// 示例
window.requestIdleCallback(callback, {timeout: 1000 });

具体的执行流程为:

  • 开发者使用requestIdleCallback方法注册对应的任务,告诉浏览器这个任务优先级不高,如果每一帧中存在空闲时间,就可以执行这个任务。浏览器执行完该任务后,如果没有剩余时间了,或者已经没有下一个可执行的任务了,那么 react 应该归还控制权。

  • 另外,为了避免浏览器繁忙导致任务一直不执行的情况,开发者可以传入timeout参数去定义超时时间,如果到了超时时间,浏览器必须立即执行。

那浏览器什么时候有空? 参考《浏览器的每一帧都需要做什么》

requestIdleCallback目前只有 chrome 支持,所以 React 团队实现了功能更完备的polyfill,这就是 Scheduler,它提供了多种调度优先级供任务设置。

polyfill是用于实现浏览器并不支持的原生API的代码。

2. requestAnimationFrame:适用于高优先级任务

requestAnimationFrame是浏览器提供的用于绘制动画的 api,它要求在浏览器在 下次重绘之前(即下一帧) 调用指定的回调函数来更新动画。

两者区别:

requestAnimationFrame的回调属于高优先级任务,在每一帧都会执行;而requestIdleCallback的回调属于低优先级任务,不一定在每帧都执行。

其他

1. Scheduler 中的优先级

自上而下,任务优先级会降低。

  • Immediate:立即执行,级别最高。
  • UserBlocking:用户阻塞级别的优先级,比如打字、鼠标拖拽。
  • Normal:正常
  • Low:较低
  • Idle:优先级最低,表示任务可以闲置

2. 深度剖析“中断又恢复”的问题

  • 中断:保存当前的处理成果,修改tag标记一下,然后迅速收尾再打开一个requestIdleCallback,下次有机会再做。
  • 断点恢复:下次处理到这个fiber时,看到tag是被打断的任务,那么就接着做未完成的部分 或者 重做

无论是时间用完了,还是被高优先级任务打断了,对中断机制来说都是一样的。


参考文章:

  1. Inside Fiber: in-depth overview of the new reconciliation algorithm in React
  2. 完全理解React Fiber
  3. Deep In React之浅谈 React Fiber 架构(一)
  4. Fiber原理介绍
  5. Fiber的六个问题

新增参考:

  1. 重点文章:走进React Fiber的世界 - 淘系前端团队
  2. 这可能是最通俗的React Fiber打开方式