从源码角度了解 React Fiber

React Fiber是一套调度机制,能够暂停工作恢复工作,并且能够为每一个工作分配优先级,恢复工作之后能够复用之前的状态

react 15 架构

  • Reconciler协调器
  • Render 渲染器

Reconciler协调器

主要通过Diff算法得到需要更新的组件

Render 渲染器

获取要更新的组件 同步 进行渲染。

React 中的渲染器主要有:

  • React DOM
  • React Native
  • React Test

React 15 渲染方式

react15同步递归更新组件的,协调器得到差异更新,同步给到渲染器进行DOM更新

由于递归更新,一旦开始,就无法停止,当组件层级很深时,递归的时间会超过16.6ms,此时交互就会卡顿。

如果在递归更新中加入可中断的异步更新,可想而知,当更新一部分UI之后,突然中断更新,那么后面的UI更新则不会更新。

因此,这种同步递归的更新方式显然不适合可中断的异步更新,从而FIber架构由此而生了。

React 16 架构

  • 架构层级
  • 渲染方式

React 16 组成部分

新的架构分为三层

  • Scheduler 调度器 react16版本新增
  • Reconciler 协调器
  • Render 渲染器

Scheduler调度器

调度任务的优先级,优先级较高的任务首先进入协调器。

调度器主要包含两个功能:

  • 时间切片
  • 任务优先级

时间切片主要模拟的requestIdleCallback,询问每一帧是否还有空余的时间来被调度的。后面详细阐述。

任务优先级就是当有更新触发时,调度器会给每一个更新打上更新的tag,优先级比较高的任务先进入协调器。后面详细阐述。

Reconciler协调器

React 15的协调器是递归同步处理虚拟DOM的,而新的架构则使用循环的方式处理虚拟DOM,通过shouldYield()来判断是否需要中断更新,也就是当前帧是否有剩余的时间需要被调度。

如果中断更新,那页面是如何解决更新不完全的问题呢?

新架构中SchedulerReconciler是在内存中运行的,而当所有的组件协调完成之后,就会把所有有副作用的组件一次性交给Render进而同步渲染。

在这种前提之下,就会把调度协调的处理分为一段一段的执行,也就时间切片,当所有的时间切片完成之后,渲染器会一次性渲染页面。

Render渲染器

当获取到所有的带有更新标记的组件之后,渲染器就会依次执行这次副作用,同步更新UI界面。

React 16 渲染方式

在理念中说到,Fiber架构是异步可中断的更新,下一章Fiber架构详解

从以下两个方面来深入探索Fiber 架构

  • Fiber 实现原理
  • Fiber 工作原理

Fiber 实现原理

  • 为什么选择Fiber
  • Fiber 的含义
  • Fiber 的结构
  • Fiber与Generator

在说明 Fiber之前,看下进程(Process)、**线程(Thread)**和 协程(Coroutine)之间的关系。

进程相当于是一个应用程序,它是CPU资源分配的最小单位。在浏览器相当于打开一个Tab,就开启了一个进程,而操作系统会给当前的Tab页分配内存,CPU等资源

线程相当于是这个应用程序中单个任务的执行,在浏览器中比如请求线程、渲染线程等等。所以一个进程中会存在多个线程,这些线程会共享进程中资源。

协程是一种轻量级的线程,而这种线程一般都会由用户来控制(也就是开发者)协程都会有属于自己的寄存器上下文和栈(独立的上下文执行栈),而对于栈的调度都是由开发者控制的,这样就可以实现中断代码执行以及何时恢复代码的执行,并且恢复执行时可以获取上次中断时状态

看到协程,是不是想到了Generator,对的,Generator是协程的一种实现。

纤程 也是一种轻量级的线程,(有时称为堆栈式协程或用户模式协作调度的线程)和无堆栈协程(编译器合成状态机)代表了两种不同的编程工具,具有巨大的性能和功能差异。

纤程也是协程,主要由开发者主动的操作堆栈,从而达到我们想要的性能优化。

为什么选择Fiber

我们知道,屏幕流畅(浏览器浏览)的刷新率是60HZ,也就是每一秒刷新60次,而刷新一次需要16.66ms,也就是每一帧流畅的执行需要16.66ms,在这一帧里执行了什么操作呢?

all-frame

  1. 首先,处理用户输入事件,尽快给用户反馈。
  2. 其次,检查定时器,看是否已经到了预定的时间,同时进行相应的回调,然后执行JS代码
  3. 查看Begin Frame(每一帧的事件),包括window.resize、滚动、媒体查询变化等。
  4. 执行requestAnimationFrame (rAF). 在绘制之前,会执行 rAF 回调。
  5. 执行布局操作,包括布局计算和更新,即元素在页面上的样式和显示方式。
  6. 执行Paint操作。获取树中每个节点的大小和位置,每个元素的内容由浏览器填充。
  7. 现在,浏览器进入空闲期。执行中注册的任务requestIdleCallback

React15版本使用同步递归渲染的方式,而这种方式在DOM层级很深的时候,执行的耗时会逐渐增加,从而导致用户交互变的卡顿。

React16版本为了解决这些问题引入Fiber将任务分成多个子任务,均衡的在每一帧中执行,在这个过程中,需要去中断任务,并且为每一个任务分配优先级,优先级高的任务先进行协调,在下次恢复任务的时候能够复用之前的状态。

引入Fiber之后,React和浏览器渲染线程之间交互如下图

all-frame

首先,React会向浏览器发起调度请求,查看当前帧是否还有剩余时间,如果有剩余时间,React就获得了主线程的控制权,按照子任务的优先级去执行单个子任务,当子任务执行结束之后,React会再次发起调度,查看当前帧是否还有剩余时间,如果有,继续执行下个任务,如果没有,则将主线程的控制权交还给浏览器。

循环往复,直到所有React更新任务执行完成,浏览器直接一次性同步渲染这些更改。

在这个调度的过程中,用户是完全感知不到浏览器的卡顿的,因为每一帧的执行时间会严格的控制在16.66ms

这种将任务分为多个子任务在每一帧执行的方式也被称之为时间切片

Fiber 的含义

主要体现在三个方面:

  • 架构层面:React 15版本协调器主要是使用同步递归的方式渲染,被称为stack reconiler;而React 16版本的协调器使用可中断的异步渲染,基于Fiber节点实现,称之为Fiber reconciler
  • 静态结构层面:Fiber就是一个JS对象,每个Fiber 节点对应一个react element(保存了组件/DOM节点的所有信息)
  • 动态结构层面:每个Fiber节点保存需要去执行的工作(是否需要更新/删除)以及新老状态等。

Fiber的结构

Fiber结构的定义是在react-reconciler单独的包中,详细解释可以看这里

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance 静态结构
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber 链表结构 产生Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// Fiber 节点的状态保存
this.pendingProps = pendingProps; // 当前需要更新的状态
this.memoizedProps = null; // 上次老的状态
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects 副作用 需要去更新的
this.flags = NoFlags;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 优先级
this.lanes = NoLanes;
this.childLanes = NoLanes;
// Fiber 双缓存指针
this.alternate = null;
}

Fiber 架构层面

每个Fiber节点都有对应的React element,对于整个React应用来讲,就相当于是Fiber树。

// 指向父级Fiber节点
this.return = null;
// 指向子级Fiber节点
this.child = null;
// 指向兄弟Fiber节点
this.sibling = null;

上述这三个属性将整个React应用构建成了一颗Fiber树,如下图:

react-fiber-tree

Fiber 的静态结构层面

React16中,react elementdom 节点都可以是Fiber节点,而这些Fiber节点会保存相关信息,比如该节点是什么类型,以及其真实DOM节点

this.tag = tag; // 组件的类型 比如 classComponent functionComponent
this.key = key; // key属性 用来做diff对比
this.elementType = null; // 组件如果被memo包裹的话,跟type
this.type = null; // 如果是function,则是函数名,如果是DOM节点,则是标签名称
this.stateNode = null; // 真实DOM节点

tag为组件的类型,目前总共有24种类型类型详细

type为整个组件本身,如是class则为对应类名,函数则为函数本身,元素的的话,则是标签名称。主要作用在协调的过程中,根据不同的类型走不同的逻辑场景。

Fiber的动态就结构层面

在整个的协调过程中,会生成一颗Fiber树,而其中的Fiber 节点的新老状态如何保存,以及最终产生具有副作用的Fiber节点的链表。

// Fiber 节点的状态保存
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
// Effects 每个Fiber节点的需要去更新的副作用
this.flags = NoFlags;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;

Fiber(纤程)与 Generator

在设计理念中提出,新版本要将同步渲染改为可中断的异步更新

而纤程的实现刚好满足这种场景,可以通过开发者来主动的控制中断恢复代码的执行。

上面提到,在JS中,Generator支持这种实现,但是为什么放弃了它?

主要有两个原因:

  • Generator是具有语法传染性的。使用*语法必须将上下文中的函数都用*语法包裹住,增加了语法开销。
  • 最重要的一点,Generator是有状态的,不能在执行中间恢复,需要再次计算,不能复用之前的状态。

Fiber 工作原理

React架构中提到,新的架构有调度器协调器以及渲染器

Fiber的工作过程中,有两个阶段render阶段commit阶段

  • render 阶段:包括调度过程以及协调过程,整个过程都在内存中进程,是可以中断
  • commit阶段:得到协调结果(具有副作用的Fiber链表),然后同步的将这些副作用更新到真实的DOM上

render 阶段

render 阶段主要分为两个过程:

  • 首次页面渲染:主要生成Fiber
  • 发生更新渲染:用户交互setState或者forceUpdate之后,会复用之前存在的Fiber 树,然后生成新的Fiber树,这里就产生了双缓存Fiber树

这里rende 阶段结束之后,会产生一条Fiber节点带有副作用的链表。

双缓存Fiber树

Fiber节点静态结构中,有一个属性,可以关联正在展示的Fiber节点正在构建的Fiber节点,这个属性是alternate

React16中,最多会存在两颗Fiber树 ,一个是页面上正在展示的,称之为current Fiber树;另一个是正在内存中构建的,被称之为workingInProgress Fiber树

React应用有一个根Fiber节点,其有一个current指针,指向的是当前页面展示的Fiber树,而当发生更新时,就会构建workInProgress Fiber树,构建结束之后,进入commit阶段之前根Fiber节点current指针就会指向构建完成的workInProgress Fiber 树;如果再发生更新的话,也会按这样的流程走。

正是这两颗Fiber树,构成了双缓存Fiber树

const App = () => {
return (
<div>
<p></p>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))

ReactDOM.render()会创建FiberRootrootFiber节点。

首次渲染时,此时没有Fiber树,所以需要第一次构建workInProgrss树,当构建完成,FiberRootcurrent会指向workInProgrss Fiber树

此时树结构为:

double-fiber-update

当发生更新时,又会重新构建workInProgress Fiber树 ,此时会复用current Fiber树中节点的状态(如果没有更新的话)

此时树结构为:

double-fiber

上述过程就是双缓存FIber树产生以及运行的过程,其中的细节如Fiber节点的产生、diff算法副作用链表的对比等,会在架构篇中阐述到。

commit 阶段

commit阶段 主要是同步操作,遍历effectList(副作用链表),然后执行每一个副作用Fiber节点

commit阶段主要有三个过程:

  • commitBeforeMutationEffects 在执行DOM之前处理
  • commitMutationEffect 在执行DOM的时候处理
  • commitLayoutEffect在执行DOM之后处理

在这个过程中,主要处理一些生命周期方法以及hooks方法等,在架构篇会详细说到。

总结

本篇主要说到Fiber架构React为什么选择Fiber,Fiber架构主要解决了react 15同步递归渲染的弊端,为了更好的实现快速响应的目标。以及介绍了Fiber的静态结构、作为动态单元如何生成双缓存Fiber树,下面着重阐述在render阶段commit阶段是如何工作的。