React 原理解析:从 Fiber 架构到 Diff 算法、Hooks 实现

带着问题去学习 React 源码

本文主要是记录了本人学习 React 原理的过程,以及一些理解和笔记。

  • 为什么 react 会引入 fiber 架构
  • 简述 fiber 节点的结构和作用
  • fiber 架构 + 流程
  • diff 算法
  • hooks 原理

react 理念

  • 核心公式:UI = render(data)
  • 目标:快速响应用户操作
  • 单向数据流:数据驱动视图,状态变更时重新渲染

我们可以反向推导出,可能存在一些导致响应慢的原因,所以才会有 react 这样设计的一些结构,就是为了解决这样的问题,如:

  1. CPU 卡顿
    • 浏览器刷新率为 60Hz → 每帧预算约 16.6ms
    • JS 执行时间过长(大量的虚拟 DOM 更新、组件更新或复杂的计算任务导致的 CPU 资源消耗过高)会占用主线程(负责 UI 渲染的线程),导致样式、布局、绘制无法在帧内完成,出现掉帧,影响应用响应速度和用户体验
  2. IO 卡顿
    • 网络请求、数据加载等异步操作导致等待
    • 需要通过区分不同操作的优先级(关键资源优先加载),或 Loading、SuspenseError Boundary 等手段优化体验

react 如何解决这个问题?

针对 CPU 卡顿

  • 时间切片:将长任务拆分为多个小任务,每个只执行约 5ms,到点让出主线程
  • 优先级调度:用户输入等高优先级任务可以插队执行
  • Concurrent Mode(并发模式):渲染过程变得可中断、可恢复,支持后台预渲染
  • 核心转变:从同步阻塞的大任务异步非阻塞、可中断的用户优先级渲染

针对 IO 卡顿

  • Loading:请求期间展示加载状态
  • Suspense fallback:组件尚未就绪时展示兜底内容
  • Error Boundary:捕获子组件渲染错误,展示降级 UI(当前需类组件实现,依赖 componentDidCatch

新老 react 架构对比

React 15 架构
  • Reconciler(协调器):递归处理 Virtual DOM Diff,找出变化组件,通知 renderer 渲染
  • Renderer(渲染器):将变化的组件渲染到 DOM
  • 问题:
    • 基于递归的方式进行虚拟 DOM 的比较和更新,而递归不可中断,组件层级深时执行时间易超 16ms,造成卡顿
    • 没有任务优先级的概念,所有更新任务按照生成的顺序依次同步执行,可能会造成用户体验的延迟和不流畅
React 16.8+ 架构

新增 Scheduler(调度器),改造 Reconciler,形成三层架构:

  • Scheduler:根据优先级调度任务,安排任务的执行顺序,实现时间切片
  • Reconciler:协调过程改为可中断的循环,遍历 Fiber 树,标记副作用
  • Renderer:同步提交副作用,更新 DOM,将更新的虚拟 DOM 转换为实际的 UI 输出

Scheduler(调度器)、Reconciler(协调器)和Renderer(渲染器)共同工作来提供React组件的渲染和更新。

React 18 架构
  • 引入 createRoot 替代 ReactDOM.render,默认开启并发模式
  • 新 API:useTransitionuseDeferredValue 等,利用优先级调度降低交互阻塞
  • 自动批处理:所有更新默认批量处理,减少不必要的渲染
  • Suspense 能力增强,支持 SSR 流式渲染与服务端组件

Scheduler 调度器

作用是根据任务的优先级,安排任务执行顺序,实现时间切片和可中断渲染

部分浏览器的原生 API 实现了类似的功能:requestIdleCallback - Web API | MDN,React 没有直接使用,而是自行实现了一套调度器

  • 任务拆分:将大型渲染任务拆分为多个小任务(每个对应一个 Fiber 节点)
  • 优先级管理:维护任务队列,按优先级调度
  • 时间切片:通过检查当前帧剩余时间决定是否暂停,交还主线程给浏览器
  • 底层控制:利用浏览器 API 实现精确的时间控制
    • requestAnimationFrame:在每一帧开始时获取时间戳,计算剩余时间
    • MessageChannel / setTimeout:用于在宏任务中调度回调,模拟 requestIdleCallback 的行为(兼顾兼容性和执行时机控制)

Reconciler 协调器

在 React 中,协调是指 决定如何高效地将新 UI(由 JSX 生成的 React Element)与旧 UI(当前 Fiber 树)进行对比,找出需要变动的部分,并生成可执行的更新序列(副作用标记)。

工作特点:

  • 更新过程变为可中断的循环,每次执行一个工作单元(Fiber 节点)
  • 每完成一个工作单元检查 shouldYield(),若当前帧剩余时间耗尽则暂停循环,留出时间给浏览器渲染
  • 下一帧空闲时恢复执行,继续遍历(由 Scheduler 调度)
  • 实现「递」和「归」两阶段遍历:
    • 「递」beginWork:深度优先,创建或更新子 Fiber,对不同类型的节点分派处理
    • 「归」completeWork:自底向上,创建真实 DOM,收集副作用标记
  • 最终通过 subtreeFlags 冒泡,让根节点知道哪些子树存在副作用,供 commit 阶段快速定位

协调过程主要包含:

  • 触发更新(setState、props 变化等)
  • 创建/获取 workinProgress 树(双缓冲机制)
  • 执行 diff 算法(比较新旧节点类型、key、props)
  • 标记副作用(在 Fiber 节点上打 flags,如 Placement、Update、Deletion)
  • 通过 bubbleProperties 将子树副作用合并到 subtreeFlags

可中断的循环(时间切片)

  • 协调过程以 fiber 节点为工作单元,在一个 while 循环中逐个处理

  • 每完成一个工作单元检查 shouldYield(),检查当前帧是否有剩余时间

  • 若时间耗尽则暂停循环,留出时间给浏览器渲染

  • 下一帧空闲时恢复执行,继续遍历(由 Scheduler 调度)

React Fiber 节点

  • Fiber 节点本质上是一个对象,使用了链表结构。每个 Fiber 节点是一个工作单元,对应一个 React Element,承载组件类型、DOM 节点、状态、副作用等信息
  • 关键属性:
    • tag:标识节点类型(函数组件、类组件、宿主元素等)
    • type:对应的组件或 HTML 标签
    • stateNode:关联的真实 DOM 或类组件实例
    • child / sibling / return链表式的树结构,支持遍历时中断和恢复
    • alternate:指向另一棵树上对应的节点,形成双缓冲
    • memoizedState / pendingProps / memoizedProps:状态(函数组件中为 Hooks 链表,类组件中为 state,宿主组件为 null)和属性快照,
    • flags / subtreeFlags:副作用标记(替代旧版 effectList)
  • 作用:
    • 作为 Fiber 架构的节点,通过链表的形式串联起来,可以支持状态更新的可中断、可恢复
    • 作为静态数据结构,每个 Fiber 节点对应一个 React element,保存了该组件的类型、对应的 DOM 节点等信息
    • 作为动态工作单元,驱动调和过程,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)

Fiber 如何更新 DOM —— 双缓冲机制

  • 同时维护两棵 Fiber 树:
    • current Fiber:屏幕上当前显示的节点对应的树
    • workInProgress Fiber:正在内存中构建的新树
    • 两棵树的对应节点通过 alternate 属性互相引用,便于对比和判断是否需改动
  • Mount(初次渲染) 时:
    • 创建 FiberRootNode,其 current 指向整个应用的根 Fiber
    • 创建 hostRootFiber,设置为 FiberRootnodecurrent 属性
    • 从根节点开始构建 workInProgress 树,完成后直接作为新的 current
  • Update(更新)时:
    • 基于 current 树克隆新的 workInProgress 树
    • Render 阶段完成比较和标记
    • Commit 阶段结束后,root.current 指向 workInProgress 树,workInProgress 变为新的 current,旧的 current 树被回收

React 渲染的两个阶段

image-20260603222054596

当用户点击按钮更新 count,Scheduler 先进行任务的调度,之后将任务交给 Reconciler 计算出新的 UI,最后由 Renderer 同步进行渲染更新操作

其中 Sceduler 和 Reconciler 的工作流程随时可能中断:

  • 有更高优先级的任务需要执行
  • 当前 time slice 没有剩余时间
  • 发生了其它错误

Render 阶段

Reconciler 的工作阶段,这个阶段会调用组件的 render 方法

目标:内存中构建一棵新的 workInProgress 树,并标记所有需要变更的节点(副作用),不操作浏览器中的真实 DOM

「递」beginWork

  • 目标:处理当前 Fiber 节点,从 rootFiber 出发,深度优先遍历,为遍历到的每个 Fiber 节点调用 beginWork 方法,该方法会根据传入的 Fiber 节点创建子 Fiber 节点,并将这两个 fIber节点连接起来
  • 对每个节点,根据 current Fiber 是否存在,决定 mount / update 分支:
    • 有 current(update 分支):说明存在旧 Fiber,进入 update 逻辑,消费 updateQueue (有多个 Update 对象),计算新的 memoizedState(状态更新后的值);执行组件函数 / 类实例的 render,得到新的 React Element / JSX;将新的 React Element 与 current.child 子 Fiber 链表进行 Diff,决定复用、更新、删除或移动子节点
    • 无 current(mount 分支):没有旧 Fiber,直接根据新的 React Element 创建全新的子 Fiber 节点,不需要进行 Diff
  • 生成 workInProgress 子 Fiber 链表,建立 child / sibling / return 链接
  • 返回第一个子 Fiber 继续递,若无子节点(遍历到了叶子节点)返回 null,则进入「归」(completeWork

「归」completeWork

  • 处理 宿主组件(如 divbutton):

    • mount:调用 document.createElement 创建真实 DOM 节点(内存中,独立的 JavaScript 对象),设置初始属性(把 JSX 上的 classNameidstyleonClick 等属性赋给这个刚创建的 DOM 元素),并通过 appendChild 将子 Fiber 对应的 DOM 节点组装到自身,在内存中形成一棵 DOM 数,最后赋值给 stateNode
    • update:对比新旧 props,将差异存入更新队列,并在 flags 中标记 Update(不新建 DOM,复用已有 DOM 节点)
  • 收集副作用标记:

    • 自身标记:flags(如 PlacementUpdateDeletion 等)
    • 子树标记:通过 bubbleProperties 将子 Fiber 的 flagssubtreeFlags 合并到当前节点的 subtreeFlags 中,使父节点能快速判断子树是否有副作用
  • 如果不存在同级 Fiber,会进入到父级 Fiber 的“归”阶段;如果存在同级 Fiber,会进入同级 Fiber 的“递”阶段

  • render 阶段输出

    • 一棵完整的 workInProgress Fiber 树,所有节点都通过 alternate 与 current 树对应节点关联

    • 一棵完整的内存 DOM 子树(仅宿主组件),尚未插入页面

    • 副作用标记(flags / subtreeFlags)已全部冒泡至根节点,供 commit 阶段高效遍历

Commit 阶段,把变化提交到屏幕(同步,不可中断)

Renderer 的工作阶段,可以类比 git commit 提交,Renderer的工作主要就是将各种副作用(flags 表示)commit 到宿主环境的 UI 中

根据 Render 阶段生成的副作用标记,分三子阶段执行 DOM 操作:

  • before mutation:DOM 操作前的准备工作(处理DOM节点渲染/删除后的 autoFocusblur 逻辑、触发getSnapshotBeforeUpdate生命周期方法、调度useEffect
  • mutation:React根据调和阶段的计算结果,实际执行DOM的增删改操作(插入、更新、删除节点)
  • layout:DOM 操作后的同步副作用处理(如执行 useLayoutEffect、类组件的 componentDidMount/Update

Diff 算法

  • 在 Render 阶段,对于需要更新的组件,React 会将本次渲染的 JSX 对象(新的 React Element)与上一次更新后对应的 Fiber 节点(旧的 Fiber)进行比较,并尝试复用 current Fiber 树中的节点,从而生成新的 WorkInProgress Fiber 树。这个过程就是 Diff 算法
  • 本质:对比 current Fiber Tree 和 JSX 对象,生成 workInProgress Fiber Tree

Key

作用:key 是 React 中用于标识节点的唯一性的一种机制。在 Diff 算法中,React 使用 key 属性来快速定位到需要更新的节点,从而提高 Diff 算法的性能

<div>
  <p key="a">a</p>
  <span key="b">b</span>
</div>
<div>
  <span key="b">b</span>
  <p key="a">a</p>
</div>

在上面的例子中,React 在比较两个 JSX 对象时,会按照从左到右的顺序进行比较。比较第一个子节点时,发现新节点的 key="b" 与旧节点的 key="a" 不同,于是立即跳出第一轮遍历。在第二轮中,React 通过 key 映射找到对应的旧节点(span 和 p),发现它们只是交换了位置,因此复用节点并标记移动,不会销毁重建。

Diff 算法的优化

为了提高 Diff 算法的性能,React 在实现时做了一些优化:

  • 避免不必要的比较:React 在比较同级节点时,会按照从左到右的顺序进行比较,从而避免出现跨层级的节点移动问题
  • 使用 key 属性:React 使用 key 属性来标识节点的唯一性,从而在比较时能够快速定位到需要更新的节点

Diff 的瓶颈及处理

完全两棵树比对的算法复杂度为 O(n³),开销巨大。React 通过三个预设限制降低复杂度:

  • 只对同级元素进行 Diff,忽略跨层级移动(销毁并重建)

  • 不同类型元素产生不同树,如 divp,直接销毁旧树并重建新树

  • 通过 key 标识子元素,使复用更稳定

Diff 的入口

  • 入口函数:reconcileChildFibers
  • 根据 newChild(JSX 对象)的类型分派不同的处理函数:
  • object(React Element)→ reconcileSingleElement
    • string / numberreconcileSingleTextNode
    • arrayreconcileChildrenArray
    • 无命中则删除所有旧节点

对应源码:packages/react-reconciler/src/ReactChildFiber.new.js

单节点 Diff

适用场景:同级只有一个节点(newChildobjectstringnumber

核心流程(以 reconcileSingleElement 为例):

  • 遍历旧节点(currentFirstChild 链表),比较 keytype
    • 如果 key 不同 → 删除该旧 Fiber,继续比较兄弟节点
    • 如果 key 相同但 type 不同 → 删除该 Fiber 及其所有兄弟节点,跳出循环
    • 如果 keytype 都相同 → 复用旧 Fiber,返回
  • 遍历结束未找到可复用节点 → 创建新 Fiber

删除逻辑区分:

  • key 相同 type 不同 → 调用 deleteRemainingChildren(删除当前及所有兄弟)

  • key 不同 → 仅删除当前节点(deleteChild),继续比较下一个兄弟

多节点 Diff

适用场景:newChild 为数组,同级有多个节点

操作类型归纳:

  • 节点更新(属性或类型变化)

  • 节点新增 / 删除

  • 节点位置移动

为什么不能用双指针?
  • 当前 Fiber 子节点为单链表(sibling 连接),不能直接使用双指针同时遍历数组和链表

React 的策略:两轮遍历

  1. 第一轮遍历:处理更新的节点
    • 同时遍历 newChildren 和旧 Fiber 链表,按序比较
    • keytype 都相同 → 复用,继续下一对
    • key 相同但 type 不同 → 删除当前旧节点及其所有兄弟节点,并立即跳出第一轮遍历
    • key 不同 → 跳出第一轮遍历
    • 遍历结束条件:newChildren 遍历完,或旧 Fiber 链表遍历完
  2. 第二轮遍历:处理非更新的节点(新增、删除、移动)
    • 首先会将剩余的旧的 Fiber 节点放入到一个 map 里,接下来去遍历剩下的 JSX 对象数组,在 map 里找能否复用
    • 情况 1newChildrenmap 都遍历完 → 无需额外操作,Diff 结束
    • 情况 2newChildren 剩余, map 中的旧 Fiber 遍历完 → 剩余节点均为新增,依次标记 Placement
    • 情况 3newChildren 遍历完, map 中的旧 Fiber 还有剩余 → 剩余旧节点均需删除,依次标记 Deletion
    • 情况 4:双方都未遍历完 → 存在位置移动

处理移动

  • 将剩余旧 Fiber 存入以 key 为键的 Map(existingChildren
  • 遍历剩余的 newChildren,通过 key 查找对应的旧 Fiber
  • 判断是否移动:
  • 参照物:最后一个可复用节点在旧数组中的位置索引lastPlacedIndex
    • 每个可复用节点在旧数组中的索引记为 oldIndex
    • oldIndex >= lastPlacedIndex → 位置不变,更新 lastPlacedIndex = oldIndex
    • oldIndex < lastPlacedIndex → 节点需要向右移动,标记 Placement

Demo 解析

  • abcdacdb
  • 第一轮:a 匹配,cb key 不同,跳出
    • 第二轮:处理剩余 cdbc 索引 2 ≥ 0 → 不动;d 索引 3 ≥ 2 → 不动;b 索引 1 < 3 → 移动
  • abcddabc
  • 第一轮:da key 不同,直接跳出
    • 第二轮:d 索引 3 ≥ 0 → 不动;a 索引 0 < 3 → 移动;b 索引 1 < 3 → 移动;c 索引 2 < 3 → 移动。最终 a, b, c 都移动到右边

结论:尽量减少节点从后往前移动的操作,可优化 Diff 性能

← 返回列表