Fiber 架构及工作流程
一、React 16 之前的不足
1、组件渲染更新的两个阶段
React 通过 render()
和 setState()
进行组件渲染和更新时,有两个阶段:
- 协调阶段 (Reconciler):React 会自顶向下通过递归,遍历新数据生成新的虚拟 DOM,然后通过 Diff 算法,找到需要更新的元素 (Patch),放到更新队列中,得到新的更新队列;
- 渲染阶段 (Renderer):遍历更新队列,通过调用宿主环境 (DOM、Native、WebGL 等) 的 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 的工作流程
ReactDOM.render()
和setState
的时候开始创建更新;- 将创建的更新加入任务队列,等待调度;
- 在 requestIdleCallback 空闲时执行任务;
- 从根节点开始遍历 Fiber 节点,并且构建 WokeInProgress Tree;
- 生成 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 是 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 tree,WokeInProgress tree 完成后会替换 current tree。
- 每更新一个节点,同时生成该节点对应的 Effect List。
- 为每个节点创建更新任务。
- 将创建的更新任务加入任务队列,等待调度:
- 调度由 scheduler 模块完成,其核心职责是执行回调。
- scheduler 模块实现了跨平台兼容的 requestIdleCallback。
- 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复。
- 最后根据 Effect List 更新 DOM:
- React 会遍历 Effect List 将所有变更一次性更新到 DOM 上。
- 这一阶段的工作会导致用户可见的变化。因此该过程不可中断,必须一直执行直到更新完成。