第 72 期 - React Fiber 架构剖析:原理、结构与调度
logoFRONTALK AI/1月3日 16:33/阅读原文

摘要

本文讲述 React Fiber 架构的应用目的、核心思想,阐述实现该架构需解决的任务碎片化、时间分片、在浏览器空闲时执行这三个问题,介绍 Fiber 对象、双缓存 Fiber 树、Scheduler 调度、Reconciler 改造,还提及 Fiber 可能存在的问题,最后总结 Fiber 架构的多方面意义以及其优秀设计方案的应用价值。

一、React Fiber 架构的应用目的与核心思想

React Fiber 架构的应用目的是实现增量渲染,而增量渲染是实现任务可中断、可恢复,赋予任务不同优先级以达成更顺滑用户体验的手段。其核心思想为可中断、可恢复与优先级。

(一)实现 Fiber 架构需解决的问题

要实现 Fiber 架构,必须解决三个问题:任务碎片化、时间分片、在浏览器空闲的时候执行。

二、Fiber 对象

(一)引入新数据结构的原因

React 15 架构使用树形结构串联虚拟 DOM 树并递归遍历,数据量大时,递归在内存和时间方面有弊端,且无法分解工作为增量单元,也不能暂停和恢复特定组件工作。虽然用循环遍历可满足中断要求,但树形结构下若没有辅助数据结构,现场保护会很复杂。

(二)Fiber 对象的结构与作用

React 官方把虚拟 DOM 树拍扁成链表形式,用 Fiber 对象作为链表节点单位。Fiber 对象是 Javascript 对象,包含 return、child 和 sibling 三个指针,连接父子兄弟节点构成单链表 fiber 树,将递归遍历改为循环遍历实现深度优先遍历。一个 Fiber 节点对应一个元素节点,它除了这三个指针还记录其他必要信息,如 DOM 节点基本信息和任务调度信息。

// packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {|
  // 作为静态数据结构,存储节点 dom 相关信息
  tag: WorkTag, // 组件的类型,取决于 react 的元素类型
  key: null | string,
  elementType: any, // 元素类型
  type: any, // 定义与此 fiber 关联的功能或类。对于组件,它指向构造函数;对于 DOM 元素,它指定 HTML tag
  stateNode: any, // 真实 dom 节点
  // fiber 链表树相关, 主要
  return: Fiber | null, // 指向他在 Fiber 节点树中的`parent`,用来在处理完这个节点之后向上返回
  child: Fiber | null, // 指向自己的第一个子节点
  sibling: Fiber | null, // 指向自己的兄弟节点,兄弟节点的 return 指向同一个父节点
  index: number, // 在父 fiber 下面的子 fiber 中的下标
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef:?string,...})
    | RefObject,
  // 工作单元,用于计算 state 和 props 渲染
  pendingProps: any, // 本次渲染需要使用的 props
  memoizedProps: any, // 上次渲染使用的 props
  updateQueue: mixed, // 用于状态更新、回调函数、DOM 更新的队列
  memoizedState: any, // 上次渲染后的 state 状态
  dependencies: Dependencies | null, // contexts、events 等依赖
  mode: TypeOfMode,
  // 副作用相关
  flags: Flags, // 记录更新时当前 fiber 的副作用(删除、更新、替换等)状态
  subtreeFlags: Flags, // 当前子树的副作用状态
  deletions: Array<Fiber> | null, // 要删除的子 fiber
  nextEffect: Fiber | null, // 下一个有副作用的 fiber
  firstEffect: Fiber | null, // 指向第一个有副作用的 fiber
  lastEffect: Fiber | null, // 指向最后一个有副作用的 fiber 
  // 优先级相关
  lanes: Lanes,
  childLanes: Lanes,
  alternate: Fiber | null, // 指向 workInProgress fiber 树中对应的节点
  actualDuration?: number,
  actualStartTime?: number,
  selfBaseDuration?: number,
  treeBaseDuration?: number,
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
  _debugNeedsRemount?: boolean,
  _debugHookTypes?: Array<HookType> | null,
|};

Fiber Node 不仅是数据节点结构,还是 React 的任务拆分单位,每个任务单元负责一个节点处理。

三、双缓存 Fiber 树

(一)双缓存技术原理

双缓存技术用于图像处理时可避免闪屏,其原理是在内存中构建当前帧,构建完直接替换上一帧。

(二)在 React 中的应用

React 中最多同时存在两棵 Fiber 树,第一次渲染后有 current Fiber 树反映当前屏幕内容,更新时在内存构建 workInProgress Fiber 树反映未来状态,构建完成后用它替换 current Fiber 树。workInProgress Fiber 树可看作工作快照,是不可见的,很多属性可复用 current Fiber 树,二者通过 alternate 属性建立关联。

function createWorkInProgress(current,...) {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(...);
  }
 ...
  workInProgress.alternate = current;
  current.alternate = workInProgress;
 ...
  return workInProgress;
}

四、Scheduler 调度

(一)requestIdleCallback

1. 基本语法与原理

基本语法为var handle = window.requestIdleCallback(callback[, options]),其 callback 会接收 deadline 对象,通过它获取浏览器空闲时间和回调是否超时状态,可合理安排帧内任务,空闲就执行,时间不足就再次请求。也可传入 timeout 配置超时时间,但尽量少用,因为会有性能损失和丢帧风险。

type Deadline = {
  timeRemaining: () => number // 当前剩余的可用时间。即该帧剩余时间。
  didTimeout: boolean // 是否超时。
}
function work(deadline:Deadline) { // deadline 上面有一个 timeRemaining()方法,能够获取当前浏览器的剩余空闲时间,单位 ms;有一个属性 didTimeout,表示是否超时
  console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
  if (deadline.timeRemaining() > 1 || deadline.didTimeout) { // didTimeout 为 true 表示是因为超时而被触发
     // 走到这里,说明时间有余,我们就可以在这里写自己的代码逻辑
  }
  // 走到这里,说明时间不够了,就让出控制权给主线程,下次空闲时继续调用
  requestIdleCallback(work);
}
requestIdleCallback(work, { timeout: 1000 }); // 这边可以传一个回调函数(必传)和参数(目前就只有超时这一个参数)

2. 存在的问题

requestIdleCallback 是实验性 API,浏览器支援度有限,语法和行为可能改变,且触发频率不稳定,切换 tab 时可能降低触发机会。

(二)requestAnimationFrame

它用于帧动画,回调在每一帧确认执行,属于高优先级任务,与屏幕刷新频率同步。

(三)Scheduler 任务调度器

1. 多个任务的管理

Scheduler 可宏观管理多个任务,微观节制单个任务执行。它引入任务优先级和时间片概念。任务有开始时间和到期时间,Scheduler 维护 6 种优先级,不同优先级对应不同 timeout 数值。任务进来时,根据开始时间和当前时间比较判断是否过期,分别推入 timerQueue(未过期队列)和 taskQueue(已过期队列),同队列任务根据不同标准排序,taskQueue 中的任务会被循环执行,timerQueue 中的任务会被检测是否过期,过期则移入 taskQueue。

2. 单个任务的执行控制

单个任务执行控制涉及任务中断和恢复,需要调度者和执行者配合。在浏览器每一帧时间里,JS 线程默认时间切片是 5ms,循环处理 taskQueue 中的任务时,每次 while 循环退出就是一个时间切片用尽,执行 task.callback 前要进行超时检测。

五、Reconciler 改造

Scheduler 调度器可将任务切片提交到 Reconciler 调和器。React 15 架构中 Reconciler 与 Renderer 交替进行,强行中断更新会导致页面更新不完全,而 Fiber 架构下 Reconciler 执行过程分为 render / reconciliation phase 与 commit phase。render 阶段工作可中断,能暂停、中止和重新开始,commit 阶段工作不可中断,因为会更新真实 DOM。在 render 阶段,React 可根据时间片处理一个或多个 fiber 节点,能保存执行到一半的工作,重新获取时间片后可继续工作。遇到高优先级任务时,render 阶段工作会被中止,重新执行更新任务前的生命周期函数,所以 render 阶段的生命周期函数尽量不要做有副作用操作。

六、Fiber 可能存在的问题

(一)生命周期函数多次执行

Fiber 更新分两个阶段,reconciliation 阶段可打断,可能导致 commit 前的生命周期函数多次执行,官方已标记部分传统类组件生命周期函数为 unsafe,推荐使用新的生命周期函数。

(二)饥饿问题

高优先级任务一直插入可能使低优先级任务无法执行,官方提出尽量复用已完成操作来缓解。

七、总结

Fiber 从不同角度有不同意义,从架构看是核心算法重写,从编码看是数据结构,从工作流看是工作单元。React Fiber 架构中的优秀设计方案可应用到平时的架构设计中。

 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有