浅谈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的升级绝对是干货满满。

欢迎关注我们的公众号