关于学习React的diff算法和Fiber架构的一些笔记

0 阅读6分钟

React Diff 算法

React Diff 算法是 React 中用于高效比较虚拟 DOM 树,从而找出最小化 DOM 更新操作的算法。它的核心目标是在 O(n) 的复杂度内完成两棵树的比较(传统树对比算法的复杂度为 O(n³))。

Diff 算法的步骤概览 (针对比较两个根节点 oldNode 和 newNode):

  1. 如果 newNode 为 null 或 undefined

    • 返回“删除节点”的操作。
  2. 如果 oldNode 为 null 或 undefined

    • 返回“创建节点”的操作。
  3. 如果 oldNode 和 newNode 不是同一个 DOM 元素(标签、组件类型不同):

    • 返回“替换节点”(即删除 oldNode,创建 newNode)的操作。
  4. 如果 oldNode 和 newNode 是同一个 DOM 元素(标签相同):

    • 比较属性:  找出新旧属性的差异,生成“更新属性”的操作。

    • 比较子节点:

      • 注: key属性是关键! React 要求为列表的每个元素提供一个 唯一且稳定的 key 属性。  key 就像是列表中每个元素的身份 ID。 当 Diff 算法比较列表时,它会根据 key 来匹配新旧节点。

      • 查找相同 key 的节点:  如果新旧列表中都有同一个 key 的节点,React 认为它们是同一个元素,然后比较它们的属性和子节点。

      • 查找新增加的 key  如果新列表中有 key,但在旧列表中不存在,则认为是一个新节点,进行插入。

      • 查找被删除的 key  如果旧列表中有 key,但在新列表中不存在,则被认为是旧节点,进行删除。

      • 递归遍历地对每一对相同 key 的子节点调用 Diff 算法。

为什么 key 必须稳定?  如果 key 每次渲染都变化,Diff 算法就无法有效地识别出哪些元素是“同一个”,反而会认为所有元素都发生了变化,导致不必要的 DOM 重建,性能更差。

不推荐使用 index 作为 key  如果列表顺序会改变(插入、删除、移动),使用 index 作为 key 会导致 Diff 算法误判,带来性能问题。只有在列表是静态的、不会改变顺序,并且没有被插入/删除元素时,index 才勉强可以接受(但仍不推荐)。


Fiber架构

一、为什么需要 Fiber?(问题背景)

旧架构(Stack Reconciler)的问题

javascript

// React 15 的递归 Diff(简化)
function render(vnode) {
  if (vnode.type === 'div') {
    const dom = document.createElement('div');
    vnode.children.forEach(child => render(child)); // 递归调用
    parent.appendChild(dom);
  }
}
// 问题:一旦开始,必须执行完整个树,无法中断

后果

  • 大组件树(1000+ 节点)会长时间占用主线程(>16ms)
  • 导致页面掉帧、用户输入卡顿
  • 无法实现优先级调度(高优先级任务必须等待)

二、Fiber 的核心数据结构

一、为什么需要 Fiber?(问题背景)

旧架构(Stack Reconciler)的问题

Stack基于传统虚拟DOM树的深度优先递归遍历,一旦开始,必须执行完整个树,除非报错,否则无法中断。

Fiber 的核心数据结构

Fiber是一个链表结构的 JavaScript 对象,基于链表的循环遍历,通过每个虚拟 DOM 节点对应一个 Fiber 节点来构建Fiber树。

// 精简的 Fiber 节点结构
type Fiber = {
  // 节点类型(div/span/函数组件等)
  type: 'div' | Function | Class,
  
  // 链表指针(核心!)
  return: Fiber | null,   // 指向父节点
  child: Fiber | null,    // 指向第一个子节点
  sibling: Fiber | null,  // 指向下一个兄弟节点
  
  // 工作进度(双缓冲)
  alternate: Fiber | null,   // 指向另一棵树中的对应节点
};

通过 child + sibling + return 三个指针,可以记住当前遍历位置,允许随时中断。

Fiber 架构的工作原理

一.【链表】→【增量渲染】→【时间切片】→【优先级插队】

链表:实现了可中断的遍历;

增量(Incremental) :把一个大任务拆成多个小任务,分批次、可间断地完成;

增量渲染:在React中:整个组件树的Diff工作被拆解成一个个小任务(每个Fiber节点就是一个任务) ;

时间切片:一种在多个帧之间动态调度执行被增量渲染拆分渲染任务形成的多个小任务,在浏览器空闲时间执行,必要时让出主线程的技术,以提高页面的响应速度和用户体验;

优先级插队:允许高优先级任务打断低优先级任务;(Lanes模型)

二.双缓存

什么是双缓存

在 React 中,双缓存是一种用于解决 UI 渲染过程中闪烁和视觉不连续的技术。传统的渲染过程中,更新操作会直接修改 DOM,导致在更新过程中用户可能会看到中间状态的 UI,造成视觉上的不连续和不稳定。双缓存技术通过在内存中维护两份 UI 状态,一份用于渲染当前帧,另一份用于计算下一帧的状态,从而避免了直接在 DOM 上进行更新操作。

双缓存Fiber树

React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树

current Fiber树中的Fiber节点被称为current fiberworkInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使current指针在不同Fiber树rootFiber间切换来完成current Fiber树指向的切换。

即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

每次状态更新都会产生新的workInProgress Fiber树,通过currentworkInProgress的不停轮换,完成DOM更新。 Fiber如何工作?可中断的“双缓存”策略

Fiber架构将协调过程分为两个截然不同的阶段:

  1. Render / Reconciliation Phase (渲染/协调阶段)

    • 可中断、可恢复、异步。  这个阶段负责计算“哪些需要更新”,但绝不操作真实DOM
    • React会在内存中构建一棵新的Fiber树,称为 WorkInProgress Tree(工作在进行树) 。它通过与当前屏幕上显示的 Current Tree(当前树)  上的Fiber节点进行Diff比较来完成构建。
    • 工作方式:React的调度器会循环处理每个Fiber单元。处理完一个单元,它就检查主线程是否还有空闲时间(通过requestIdleCallbackscheduler)。如果没有时间了,或者有更高优先级的任务(如用户输入),React就立刻中断当前工作,保存进度(下一个要处理的Fiber),把主线程交还给浏览器。等浏览器忙完了,React再回来从断点继续。
    • 这个阶段可能会被打断多次。
  2. Commit Phase (提交阶段)

    • 不可中断、同步执行。  这个阶段是React将协调阶段计算出的所有副作用(即需要更新的操作列表)一次性、同步地应用到真实DOM上的阶段。

    • 因为这个阶段会实际操作DOM,而DOM的变更会立刻触发浏览器的重绘重排,所以必须快速完成,用户不会看到“更新到一半”的UI。

    • 一旦开始提交,React就会一口气完成所有DOM操作。