🔥🔥🔥 React18 源码学习 - Fiber 架构

23 阅读8分钟

前言

本文的React代码版本为18.2.0

可调试的代码仓库为:GitHub - yyyao-hh/react-debug at master-pure

React16开始,引入了Fiber架构。它彻底重构了React的调和机制,将不可中断的递归遍历,革新为可暂停、可恢复的链表遍历。这一根本性变革为React带来了时间切片、并发模式等能力,使其从“同步渲染”迈入了“异步渲染”的新纪元。本文将揭示这场革命的内核原理。

为什么使用 Fiber 架构?

React 核心思想

作为一个构建用户界面的JavaScript库,React的核心思想可以概括为:

内存中维护一颗虚拟DOM树,数据变化时自动更新虚拟DOM,然后通过Diff算法对比新旧虚拟DOM树,找出变化的部分并批量更新到真实DOM 。在React16之前,这个过程被分为两个阶段:调和阶段(Reconciler)和渲染阶段(Renderer )。

旧架构的不足

React16之前,在调和阶段(Reconciler)使用递归方式同步遍历虚拟DOM树,对比每个组件以确定需要更新的部分。这个过程React团队称为Stack Reconciler,因为它依赖于JavaScript内置的调用栈。这种递归模型虽然直观,但有一个致命缺陷:一旦开始就无法中断。如果组件树层级很深,递归调用会长时间占用JavaScript主线程,导致用户交互、动画等任务无法及时响应,造成页面卡顿。

浏览器渲染机制

要理解为什么长时间占用JavaScript线程会导致卡顿,我们需要了解浏览器的渲染机制。页面是一帧一帧绘制出来的,一般浏览器的刷新频率为60hz,即每秒绘制的60帧(FPS),这意味着每一帧只有约16.7ms的时间来执行所有任务。

在一帧之内,浏览器需要完成多个步骤:处理用户交互、JavaScript解析执行、requestAnimationFrame调用、布局、绘制等。如果JavaScript执行时间过长,超过了16ms,就会延迟后续的布局和绘制,导致帧率下降,用户自然会感觉到卡顿。

Fiber 的解决方案

为了解决Stack Reconciler的瓶颈,React团队从16 年开始重构协调算法,最终在React16中引入了Fiber Reconciler架构。

Fiber架构是用于实现虚拟DOM和组件协调的新的架构。核心理念是将渲染任务拆分为多个小的工作单元,而不是一次性同步处理整个组件树。旨在优化渲染过程、实现异步渲染,并提高应用的性能和用户体验。

Fiber架构通过实现以下目标来解决性能问题:

  • 暂停工作,稍后再回来:可以中断渲染过程处理高优先级任务
  • 为不同类型的工作分配优先级:确保用户交互和动画优先执行
  • 复用以前完成的工作:提高渲染效率
  • 如果不再需要,则中止工作:避免不必要的渲染

这种增量渲染的方式使得React能够在时间分片中处理更新,充分利用浏览器的空闲期执行任务,从而避免阻塞关键的用户交互。

Fiber新老架构.png

React官方以漫画的方式形象的展示了两种架构运行区别。来源:Fiber Reconciler

Fiber 架构深度剖析

什么是 Fiber

Fiber可以从两个互补的角度来理解:执行单元和数据结构

  • 作为一个执行单元,Fiber代表一个可以拆分的工作块。React将整个视图的更新过程分解为多个小的Fiber任务,每个任务通常只处理一个组件。与一次性完成整个更新不同,Fiber使React能够将工作分成小块,在浏览器空闲时执行它们。
  • 作为一种数据结构,Fiber是一个JavaScript对象,它由对应的React Element生成,包含了组件的详细信息及其输入和输出。从代码层面看,每个Fiber节点对应一个React元素,存储了组件的类型、状态、副作用等信息。

下面是Fiber节点的核心属性结构:

/* src/react/packages/react-reconciler/src/ReactFiber.old.js */

export type Fiber = {|
  
  /* ************ 实例的静态属性 ************ */
  
  tag: WorkTag,       // 节点类型
  key: null | string, // 用于协调算法的唯一标识
  elementType: any,   // 原始元素类型 (函数/类本身)
  type: any,          // 解析后的元素类型 (可能被babel处理过)
  stateNode: any,     // 关联的真实实例 (DOM节点/类组件实例)

  /* ************ Fiber 链表结构 ************ */
  
  return: Fiber | null,  // 指向父节点
  child: Fiber | null,   // 指向第一个子节点
  sibling: Fiber | null, // 指向兄弟节点
  index: number,         // 在子节点中的索引
  
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,

  /* ************ 属性 & 状态 ************ */
  
  pendingProps: any,
  memoizedProps: any,
  updateQueue: mixed,
  memoizedState: any,
  dependencies: Dependencies | null,

  mode: TypeOfMode,

  /* ************ 副作用与更新标记 ************ */
      
  flags: Flags,
  subtreeFlags: Flags,
  deletions: Array<Fiber> | null,

  nextEffect: Fiber | null,

  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  /* ************ 调度优先级 ************ */
  lanes: Lanes,
  childLanes: Lanes,

  /* ************ 双缓冲机制 ************ */
  alternate: Fiber | null,
|};

协调机制

Fiber架构最核心的改变之一是用链表遍历替代了递归遍历。传统的递归遍历依赖于JavaScript的调用栈,而Fiber实现了自己的堆栈帧管理,通过childsiblingreturn三个指针构建了一个树形链表结构。

return: Fiber | null,   // 指向父节点
child: Fiber | null,    // 指向第一个子节点
sibling: Fiber | null,  // 指向兄弟节点
index: number,          // 在兄弟节点中的位。

如果当前的App组件如下:

function App() {
  return (
    <div>
      Study
      <span>React</span>
    </div>
  );
}

则对应的Fiber结构如下图:每个节点只保存它的第一个子节点,其他子节点通过sibling指针单向存储,所有子节点都会通过return指针指向父节点。

App的Fiber树.png

这种链表结构使得React可以手动控制遍历过程,而不依赖于JavaScript的调用栈。遍历算法基于深度优先原则,但不再是递归实现。具体遍历相关的内容,将在下一节详细展开。

双缓存技术

Fiber架构采用双缓存技术,在内存中维护着两棵Fiber树:current treeworkInProgress tree

  • current tree:当前屏幕上显示内容对应的Fiber
  • workInProgress tree:正在内存中构建的新的Fiber

而应用的根节点(FiberRootNode)会通过current指针完成current tree的切换。

当开始一次更新时,React会从current树的根节点开始,为每个节点创建一个alternate(替代)节点,这些alternate节点就构成了workInProgress树。如果节点没有更新,React会复用previous fiber(前一个fiber)来最小化工作量。

初始化阶段 mount

  1. 容器挂载阶段

上节讲过,首先会通过ReactDOM.createRoot创建FiberRootNodeRootFiberNode

  • FiberRootNode:整个应用的根容器
  • RootFiberNodeFiber树的根节点(根组件<App/>的父节点)

由于是初始化渲染,页面中没有任何DOM,所以RootFiber没有任何子节点

初始化根容器.png

  1. Render阶段

会根据组件的React Element在内存中构建wip tree。构建时会尝试复用current tree中的Fiber节点(具体的复用过程,会在diff算法章节详细展开)。

首次构建fiber树.png

初始化渲染时,wip tree中只有RootFiberNode存在对应的alternate节点。

  1. Commit阶段

此时页面DOM更新为右侧tree对应的数据。current指针指向wip tree,使其变为current tree

首次构建切换fiber树.png

更新阶段 update

  1. 触发更新:Render阶段

构建一棵新的wip tree

更新时候的fiber树.png

  1. 触发更新:Commit阶段

此时页面DOM更新为左侧tree对应的数据。current指针指向wip tree,使其变为current tree

更新时候切换fiber树.png

这种双缓存机制有两个主要优点:

  1. 减少内存分配:通过节点复用,减少不必要的垃圾回收
  2. 无缝切换:当workInProgress tree构建完成时,直接将它作为新的current tree,避免页面闪烁

后面再提到workInProgress多简称为wip

渲染流程

Fiber架构将渲染过程分为两个截然不同的阶段:Render阶段和Commit阶段。

Render阶段是可中断的异步过程,负责处理组件的渲染和Diff计算。这个阶段React执行以下操作:

  • 更新状态和属性
  • 调用生命周期方法(如getDerivedStateFromProps
  • 执行渲染函数获取子元素
  • 对比新旧子元素(Diff算法)
  • 标记需要更新的副作用(effect

由于Render阶段可能被高优先级任务中断,React使用副作用列表(effect list)来跟踪哪些节点需要更新。这是一个线性链表,包含了所有有副作用的Fiber节点,大大提高了commit阶段的效率。

Commit阶段是同步不可中断的,负责将Render阶段计算的结果应用到DOM上。这个阶段主要包括:

  • 执行DOM的增删改操作
  • 更新refs
  • 调用生命周期方法(如componentDidMountcomponentDidUpdate
  • 执行useEffectuseLayoutEffect的清理和触发函数

将这两个阶段分离确保了用户界面的稳定性:Render阶段可以多次中断和重试,但Commit阶段总是快速且同步的,确保DOM更新的一致性和原子性。

此处略做总结,实现原理都将在后续的文章中详细展开

总结

Fiber架构是React的一次重大革新,它通过重新实现协调算法,解决了大型应用场景下的性能瓶颈和用户体验问题。Fiber的核心价值可以总结为:

  1. 可中断的异步渲染:将渲染工作拆分为小任务,避免长时间阻塞主线程
  2. 基于优先级的调度:确保高优先级任务(如用户交互)快速响应
  3. 更流畅的用户体验:通过时间分片技术减少页面卡顿

下一章我们将了解Fiber树的构建过程:Render阶段