我所理解的React Fiber架构

2,068 阅读6分钟

前言

在 react16 没有正式公布以前,业界的人员以为这次的 react16 就叫做 Fiber,足以说明 Fiber 的重要性。fiber 在英文中意为纤维,此处意为比线程还细的单位。Facebook 取名 Fiber 的意思是为了描述一个比线程更小单位的渲染机制。用一句话来描述我所理解的 Fiber 架构:

新的 Fiber 架构改变了之前 react 同步的组件渲染机制,使原来同步渲染的组件现在可以异步化,可中途中断渲染,执行更高优先级的任务,用户体验更好。

为了解决什么样的问题

每次更新都不是无病呻吟的改变,而是为了解决问题,Fiber 的引入主要是为了解决在网页里面用户和网页应用进行交互的问题:

  • 在 react16 之前的版本中,组建的渲染是同步的动作,如果组件包含很多层子组件,渲染时间比较长,在组件渲染的过程中又无法被打断,会导致这期间用户无法与网页进行交互。
  • 所有的任务都是按照先后顺序,没有优先级可言,这样就会导致优先级比较高的任务无法优先被执行。

react15 存在的问题

我们用下面的一个案例来描述 react15 存在的问题: 在一个页面元素很多,且需要频繁刷新的情况下,react15 会出现失帧的情况,如下图: demo地址:claudiopro.github.io/react-fiber…

分析出现这个问题的原因:

其根本原因是:大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用 setState 更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新UI,整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过16毫秒,就容易出现掉帧的现象。 针对这一问题,React 团队从框架层面对 web 页面的运行机制做了优化,得到很好的效果。改变之后的效果见下图:

react 的解题思路:

解决主线程长时间被 JS 运算占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后,再将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。

react16 的新答卷

react 框架内部的运作可以分为三层:

  • Virtual DOM 层:描述页面长什么样
  • Reconciler 层:负责调用组件声明周期方法,进行 Diff 运算等。
  • Renderer 层:根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

这里改动最大的当属 Reconciler 层了,为了和之前的做区别,我们有了如下称呼的约定:

  • Stack Reconciler(react15 的Reconciler)
  • Fiber Reconciler(react16 的Reconciler)

Stack Reconciler 的运行过程

Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑:

Fiber Reconciler 的运行过程

Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行:

Fiber Reconciler 是如何实现的

任务优先级

为了达到这种效果,需要有一个调度器(schedular)来根据任务的优先级进行任务的分配。优先级高的任务(如键盘输入)可以打断优先级低的任务的执行,从而更快的生效。任务的优先级分为六种:

  • synchronous:与之前的Stack Reconciler操作一样,同步执行
  • task:在 next tick 之前执行
  • animation:下一帧立即执行
  • high:在不久的将来立即执行
  • low:稍微延迟执行也没有关系
  • offscreen:下一次 render 时或者 scroll 时才执行

Fiber Reconciler 的执行过程

在react的生命周期中,以 render 为界限,分为两个阶段,分别为:

  • render phase: 这一阶段的生命周期是可以被打断的,每隔一段时间就会跳出当前进程,去看下是否有优先级更高的任务在等待执行
  • commit phase: 这一阶段的生命周期是不可以被打断的,react 会将所有的变更一次性更新到 DOM 上

了解下 render phase 的机制

render phase 这个阶段所做的事非常重要,因此我们需要了解下 render phase 的机制。

  • 不被打断: 如果不被打断,那么在 render phase 阶段执行完会直接进入 render 函数,构建真实的 virtualDomTree
  • 被打断: 如果组件在 render phase 过程中被打断,react 会放弃当前组件干到一半的事情,去做更高优先级的任务。当高优先级任务执行完之后,react 通过 callback 回到之前渲染到一半的组件,从头开始渲染。(看起来放弃已经渲染完的生命周期,会有点不合理,反而会增加渲染时长,但是react确实是这么干的)

这种方式的问题

因为 render phase 阶段会被打断的特性,所以 render phase 阶段的生命周期函数可能会被执行多次。因此,我们需要保证 render phase 阶段的生命周期函数是没有副作用的纯函数,确保每次执行都是一样的输出结果。

写在最后

Facebook比较慎重,16的第一个版本并没有启用这个功能,防止改变太大,影响到别的开发人员的正常工作。在 react16 晚一些的版本中会给出一个选项可以开启 fiber,在默认情况下是关闭的。

另外,facebook 在 react16 增加 Fiber 结构,其实并不是为了减少组件的渲染时间,事实上也并不会减少,反而会增加。解决的是使得一些更高优先级的任务能够优先执行,提高用户的体验,从用户的角度不会感觉到卡顿。

最后,还有一些问题并没有得到很好地解释,比如饥饿问题,即如果优先级高的任务一直存在,那么优先级低的任务将一直没有办法执行。我们期待 react 能给出优秀的解决方案。