Skip to main content

Fiber 架构及工作流程

一、React 16 之前的不足

1、组件渲染更新的两个阶段

React 通过 render()setState() 进行组件渲染和更新时,有两个阶段:

  • 协调阶段 (Reconciler):React 会自顶向下通过递归,遍历新数据生成新的虚拟 DOM,然后通过 Diff 算法,找到需要更新的元素 (Patch),放到更新队列中,得到新的更新队列;
  • 渲染阶段 (Renderer):遍历更新队列,通过调用宿主环境 (DOMNativeWebGL 等) 的 API,更新渲染对应元素。

JavaScript 引擎和页面渲染引擎的两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待(点击查看 JS 引擎和渲染引擎详情

在协调阶段,React 16 之前使用的是 Stack Reconciler(栈协调器)。这种方式一旦任务开始进行就无法中断,JS 将一直占用主线程,一直要等到整棵虚拟 DOM 树计算完成后,才把执行权交给渲染引擎,这就导致用户交互可能出现卡顿,影响用户体验。而 React 16 之后使用的是 Fiber Reconciler(纤维协调器),将递归中无法中断的更新,重构为迭代中异步可中断的更新,从而能够更好的控制组件的渲染。

2、导致卡顿的原因分析

浏览器每秒绘制的帧数(FPS)小于 60 时,页面就会出现卡顿。而 1s >= 60 帧,相当于每一帧的工作不能超过 16 ms。浏览器一帧的工作如下:

可以看到,一帧内需要完成六个步骤的任务:

  • 处理用户的交互
  • JS 解析执行
  • 帧开始。窗口尺寸变更,页面滚去等的处理
  • rAF requestAnimationFrame
  • 布局
  • 绘制

如果协调阶段花的时间过长,即 JS 执行的时间过长,就可能导致用户有交互时,本该渲染下一帧但当前帧还在执行 JS,使用户交互无法即时得到反馈,产生卡顿感。

二、Fiber 的定义

由于浏览器是一帧一帧执行的,在两个执行帧之间,主线程通常会有一小段空闲时间,requestIdleCallback 可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务:

React Fiber 发布于 React 16,主要做了以下的操作:

  • 为每个任务增加了优先级,优先级高的任务可以中断低优先级的任务,然后再重新执行优先级低的任务;
  • 增加了异步任务,调用 requestIdleCallback API,浏览器空闲时执行;
  • dom diff 树变成了链表,一个 dom 对应两个 Fiber(一个链表),对应两个队列,以找到被中断的任务,重新执行。

因此,

从架构角度来看,Fiber 是对 React 协调阶段核心算法的重写;

从编码角度来看,Fiber 是 React 内部所定义的一种数据结构,每个 Fiber 节点对应一个 React Element,包含了组件的类型、对应的 DOM 节点、元素的更新操作队列等信息,可以看作是 React 16 新架构下的虚拟 DOM。其数据结构如下:

type Fiber = {
// 用于标记 Fiber 的 WorkTag 类型
// 表示当前 Fiber 代表的组件类型,如 FunctionComponent、ClassComponent 等
tag: WorkTag,

// ReactElement 里面的 key
key: null | string,

// ReactElement.type,调用 createElement 的第一个参数
elementType: any,

// 表示当前代表的节点类型
type: any,

// 表示当前 FiberNode 对应的 element 组件实例
stateNode: any,

// 指向 Fiber 节点树中的 parent,用来在处理完这个节点之后向上返回
return: Fiber | null,

// 指向自己的第一个子节点
child: Fiber | null,

// 指向自己的兄弟结构,兄弟节点的 return 指向同一个父节点
sibling: Fiber | null,

index: number,

ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,

// 当前处理过程中的组件 props 对象
pendingProps: any,

// 上一次渲染完成之后的 props
memoizedProps: any,

// Fiber 对应的组件产生的 Update 会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,

// 上一次渲染的时候的 state
memoizedState: any,

// 一个列表,存放这个 Fiber 依赖的 context
firstContextDependency: ContextDependency<mixed> | null,

mode: TypeOfMode,

// Effect
// 用来记录 Side Effect
effectTag: SideEffectTag,

// 单链表用来快速查找下一个 side effect
nextEffect: Fiber | null,

// 子树中第一个 side effect
firstEffect: Fiber | null,

// 子树中最后一个 side effect
lastEffect: Fiber | null,

// 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
expirationTime: ExpirationTime,

// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,

// Fiber 的版本池,即记录 Fiber 更新过程,便于恢复
alternate: Fiber | null
}

三、Fiber 的工作流程

  1. ReactDOM.render()setState 的时候开始创建更新;
  2. 将创建的更新加入任务队列,等待调度;
  3. requestIdleCallback 空闲时执行任务;
  4. 从根节点开始遍历 Fiber 节点,并且构建 WokeInProgress Tree
  5. 生成 EffectList,根据 EffectList 更新 DOM。

详细过程如下:

  • 第一部分:ReactDOM.render() 方法开始,把接收的 React Element 转为 Fiber 节点,并为其设置优先级,创建 Update,加入到更新队列中,这部分主要是做一些初始数据的准备;
  • 第二部分:主要是三个函数:scheduleWork(安排工作)、requestWork(申请工作)、performWork(正式工作),React 16 新增的异步调用的功能在这部分实现;
  • 第三部分:这部分是个大循环,遍历所有的 Fiber 节点,通过 Diff 算法计算所有更新工作,产出 EffectList 给到 commit 阶段使用,这部分的核心是 beginWork 函数,这部分基本就是 Fiber Reconciler ,包括 reconciliation 和 commit 阶段。

点击查看 Fiber 详情

四、总结

Fiber 是什么?

Fiber 是 React 16 之后采用的新协调(reconciliation)引擎,主要是为了支持虚拟 DOM 的渐进式渲染。从架构角度来看,Fiber 是对 React 协调阶段核心算法的重写;从编码角度来看,Fiber 是 React 内部所定义的一种数据结构,每个 Fiber 节点对应一个 React Element。

Fiber 解决的问题?

浏览器中 JS 的运行环境是单线程的,一旦任务耗时过长,就会阻塞其他任务的执行,导致浏览器无法及时响应用户的操作,影响用户体验。为了解决这个问题,React 16 推出了 Fiber Reconciler 架构,替代了原有的 Stack Reconciler,把耗时过长的任务进行分片,每个任务片的运行时间很短,运行完成后都会给其他任务执行的机会。这样,唯一的线程就不会被独占,其他任务也有机会执行。除此之外,对任务划分了优先级,优先调度高优先级的任务,调度过程中,还可以对任务进行挂起、恢复、终止等操作。

Fiber 对原先 React 使用带来的影响?

由于 Fiber 采用了全新的调度方式,任务的更新过程可能会被打断,这意味着在组件更新过程中,render 及其之前的生命周期函数可能会调用多次。因此,shouldComponentUpdate 生命周期函数不应出现副作用。

Fiber 的工作原理?

  • ReactDOM.render()setState() 时开始创建或更新 Fiber 树;
  • 然后从根节点开始遍历 Fiber 节点树,并且构建 WokeInProgress Tree
    • 本阶段可以暂停、终止、和重启,会导致 react 相关生命周期重复执行。
    • React 会生成两棵树,一棵是代表当前状态的 current tree,一棵是待更新的 WokeInProgress tree
    • 遍历 current tree,重用或更新 Fiber Node 到 WokeInProgress treeWokeInProgress tree 完成后会替换 current tree。
    • 每更新一个节点,同时生成该节点对应的 Effect List。
    • 为每个节点创建更新任务。
  • 将创建的更新任务加入任务队列,等待调度:
    • 调度由 scheduler 模块完成,其核心职责是执行回调。
    • scheduler 模块实现了跨平台兼容的 requestIdleCallback
    • 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复。
  • 最后根据 Effect List 更新 DOM:
    • React 会遍历 Effect List 将所有变更一次性更新到 DOM 上。
    • 这一阶段的工作会导致用户可见的变化。因此该过程不可中断,必须一直执行直到更新完成。