浅谈 React 16 中的 Fiber 机制

九月份关于 React 开源协议的话题在社区闹得沸沸扬扬,所幸最后以 Facebook 宣布遵循 MIT 进行开源告终,但由此也足以看出 React 在前端圈的影响力。

作为 Facebook 前端出品的当门红,React 也迎来了一次大的升级,引入了最新的核心调度算法即 Fiber 机制,这是 React 基于 Fiber 调度的第一个版本,其次还增加了一些呼声高的新特性,如 render 方法返回数组、 debug 增加组件错误堆栈追踪等。本篇将主要介绍最新的 Fiber 调度机制。

React 目前面临的挑战

React 渲染页面分为两个阶段:

  • 调度阶段(reconciliation):在这个阶段 React 会更新数据生成新的 Virtual DOM,然后通过 Diff 算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。
  • 渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到 DOM 上。

现有 React 一个非常大的问题是调度阶段是不可控的,什么意思?

假如我们更新一个 state,有 1000 个组件需要更新,每个组件更新需要 1ms,那么我们就会有将近 1s 的时间,主线程被 React 占着用来调度,这段时间内用户的操作不会得到任何的反馈,只有当 React 中需要同步更新的任务完成后,主线程才被释放。对于这 1s 内 React 的调度,我们是无能为力的。

整个调度过程就如下图所示,组件树一旦过大,就会出现浏览器失去响应的情况,用户体验非常差。

Lin Clark 在 Fiber 分享视频中对这一问题做了讲解,横向代表一次 state 变动需要做的任务,纵向代表在该时刻,主线程的占用情况。我们可以看到,随着 React 同步任务的执行,主线程将会一直被占用,无暇顾及其他任务。

Fiber 解决方案

Fiber 的中文解释是纤程,是线程的颗粒化的一个概念。也就是说一个线程可以包含多个 Fiber。

Fiber 的出现使大量的同步计算可以被拆解异步化,使浏览器主线程得以调控。从而使我们得到了以下权限:

  • 暂停运行任务。
  • 恢复并继续执行任务。
  • 给不同的任务分配不同的优先级。

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

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

得益于同步任务的拆分,主线程可以短时间内被释放进行下一次的调用,不会出现页面失灵现象

Fiber 的实现原理

React Fiber 的做法是不使用 Javascript 的栈,而是将需要执行的操作放在自己实现的栈对象上。这样就能在内存中保留栈帧,以便更加灵活的控制调度过程,例如我们可以手动操纵栈帧的调用。这对我们完成调度来说是至关重要。

大致上 Fiber 在调度的时候会执行如下流程:

  1. 将一个state更新需要执行的同步任务拆分成一个 Fiber 任务队列
  2. 在任务队列中选出优先级高的 Fiber 执行,如果执行时间超过了deathLine,则设置为pending状态挂起状态
  3. 一个 Fiber 执行结束或挂起,会调用基于requestIdleCallback/requestAnimationFrame实现的调度器,返回一个新的 Fiber 任务队列继续进行上述过程

requestIdleCallback会让一个低优先级的任务在空闲期被调用,而requestAnimationFrame会让一个高优先级的任务在下一个栈帧被调用,从而保证了主线程按照优先级执行 Fiber 单元。

不同类型的任务会被分配不同的优先级,以下是关于优先级的定义:

module.exports = {
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. 
  TaskPriority: 2, // Completes at the end of the current tick.
  AnimationPriority: 3, // Needs to complete before the next frame.
  HighPriority: 4, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 5, // Data fetching, or result from updating stores.
  OffscreenPriority: 6, // Won't be visible but do the work in case it becomes visible.
};

由此我们可以看出Fiber任务的优先级顺序为:

文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务

结语

由于 Fiber 的官方文档还未完成,所以笔者也只能从 Fiber 开发者的博客,存档及会上的分享中寻找相关实现细节,但是毫无疑问这次 React 的升级绝对是干货满满。