React 虚拟 DOM Diff 算法详解,Vue、Snabbdom 与 React 算法对比

React 虚拟 DOM Diff 算法详解

React 的 Diff 算法基于启发式原则,将一般的树比较问题简化为线性时间。其核心假设包括:只比较同一层级的节点,不跨层级重用节点不同类型的元素视为全新节点,不做局部复用使用 key 提示哪些子节点在不同渲染中保持稳定 blog.csdn.netlegacy.reactjs.org。在实践中,这意味着 React 实现了一个 O(n) 的算法:首先比较根节点,如果类型(或 key)不同,则销毁旧树、重新构建新树legacy.reactjs.org;如果类型相同,则保留该 DOM 实例,仅更新其属性(props/attributes)后递归对子节点执行 difflegacy.reactjs.orglegacy.reactjs.org。与此对应,React 只在同一层级内一一比较子节点列表,否则视为整个子树替换blog.csdn.netdevpress.csdn.net。这一原则有效避免了传统树比较的 O(n^3) 复杂度问题devpress.csdn.net。此外,key 提供了稳定的标识:当列表中存在 key 时,React 会根据 key 先建立映射,再对比匹配的节点;移动操作会复用节点并调整位置 ,避免不必要的删除和创建legacy.reactjs.orgdevpress.csdn.net

  • 同级比较(same-level) :React 只在同一层级的子列表中做比较,不会跨层寻找相似节点blog.csdn.netdevpress.csdn.net。如果一个节点在前后两次更新中层级发生变化,算法不会尝试跨层匹配,而是直接将该子树整体替换,这一点依赖于假设"跨层变化很少见"devpress.csdn.net

  • 类型不同即重建 :若新旧两节点类型(或组件类型)不同,React 视为完全不同的节点,销毁旧树所有子节点再创建新树legacy.reactjs.orgblog.csdn.net。例如,将 <div> 换成 <p>,或换用不同的组件,都会触发全量卸载与重建。

  • 键值匹配(key)key 是区别同层节点的唯一标识。当子节点带有稳定的 key 时,React 会以 key 为基准匹配旧节点和新节点,并据此移动或复用 DOM 节点legacy.reactjs.orgdevpress.csdn.net。例如,在列表头部插入元素时,如果使用了 key,React 能识别出其他节点"只是位置变化",仅新增头部节点;若未使用 key,则可能误将所有节点都当作替换和重新渲染,效率较低legacy.reactjs.orglegacy.reactjs.org

节点对比的具体流程

React 的 Diff 过程自上而下递归进行,对每个节点执行如下步骤:

  1. 节点匹配判定 :比较新旧节点的类型和 key。若两个节点是完全相同的对象(指针相同)或(对 Vue/Snabbdom 可选)都标记为静态节点且 key 相同,则可直接复用,跳过后续更新jonny-wei.github.iovuejs.org。否则进入下一步。

  2. 类型不同处理 :若新旧节点类型不同(或 key 不同),React 立即销毁旧节点所在子树并创建新节点legacy.reactjs.orgblog.csdn.net。旧节点的所有 DOM 子树卸载,组件执行 componentWillUnmount;新节点创建完成后,执行 componentDidMount

  3. 属性更新 :若节点类型相同,则更新该节点的属性(props/attributes)。对于 DOM 元素,会比较属性差异,逐一更新变化的属性或样式legacy.reactjs.org;对于组件节点,会更新组件实例的 props 并调用相应生命周期(如 componentDidUpdate),然后获取组件新的 render 输出。

  4. 子节点递归 :接下来递归处理该节点的所有子节点列表。如果新的子节点列表为空而旧列表非空,则卸载所有旧子节点;反之,若新列表有额外节点,则创建并插入这些节点。默认情况下,React 会同时遍历 旧列表和新列表,按索引依次比较差异legacy.reactjs.org。例如,在列表末尾添加元素时,前面的节点匹配后只需插入末端新节点,开销很小legacy.reactjs.org;但如果在头部插入,React(无 key 时)会认为第一个元素变了,因此导致后续元素全部移动,效率较差legacy.reactjs.org

  5. 键控列表优化 :当子节点包含 key 时,React 会使用键进行精确匹配。实现时通常先构建旧子节点的 key->index 映射表,再遍历新子节点列表:对于每个新节点,查表找到是否存在对应的旧节点索引。如果找不到(新节点),则创建新 DOM;找到则复用该旧节点并调用递归 diff,然后将旧节点位置可能移到当前索引前legacy.reactjs.orgblog.csdn.net。遍历结束后,多余的旧节点会被删除。通过 key,React 能准确识别节点移动而非简单替换,从而只对位置变化的节点进行 DOM 操作,避免大范围的重建legacy.reactjs.orgdevpress.csdn.net

示例代码: 下面给出一个简化的 JavaScript 伪代码示例,演示上述 diff 逻辑。注意这仅为示例,真实 React 源码比这复杂:

复制代码
javascript 复制代码
function diff(oldNode, newNode) {
  if (!oldNode) {
    // 新节点,无旧节点:创建新节点并插入
    mount(newNode);
  } else if (!newNode) {
    // 旧节点无对应新节点:删除旧节点
    unmount(oldNode);
  } else if (oldNode.tag !== newNode.tag || oldNode.key !== newNode.key) {
    // 类型或 key 不同:替换节点
    replaceNode(oldNode, newNode);
  } else {
    // 类型相同:复用节点,更新属性
    updateProps(oldNode, newNode);
    // 递归 diff 子节点列表
    diffChildren(oldNode.children || [], newNode.children || []);
  }
}

function diffChildren(oldChildren, newChildren) {
  // 构建旧子节点的 key 映射
  const oldKeyMap = {};
  oldChildren.forEach((child, idx) => {
    if (child.key != null) oldKeyMap[child.key] = idx;
  });
  // 遍历新子节点
  newChildren.forEach((newChild, newIdx) => {
    const key = newChild.key;
    const oldIdx = (key != null ? oldKeyMap[key] : null);
    if (oldIdx != null) {
      // 找到可复用的旧节点,递归 diff
      diff(oldChildren[oldIdx], newChild);
      // (可选:如果需要移动位置,则将 oldChildren[oldIdx] 从原位移动到 newIdx)
      oldChildren[oldIdx] = null;  // 标记为已处理
    } else {
      // 没有对应旧节点:创建新节点并插入
      mount(newChild, newIdx);
    }
  });
  // 删除未处理的旧节点
  oldChildren.forEach(child => {
    if (child) unmount(child);
  });
}

上例中,diff 函数检查节点是否存在、类型是否相同,然后分别调用对应操作。diffChildren 函数演示了基于 key 的子节点匹配:首先创建一个旧节点映射,再遍历新节点列表进行对比和复用,最后移除多余旧节点。通过类似逻辑,React 只对必要的节点执行更新或移动操作。

图:Diff 算法更新节点的流程(伪代码示意) 。上图展示了 diff 过程中更新(patch)节点的判断逻辑:首先检查新旧节点是否完全相同或静态不变(若是,则直接复用)jonny-wei.github.io;否则,如果新节点是文本节点且内容不同,则只更新文本;否则递归进入子节点对比并更新其属性或子树jonny-wei.github.iojonny-wei.github.io。这些分支逻辑确保了对于不同情况(静态节点、文本节点、复合节点)采取最小的更新策略。

Vue、Snabbdom 与 React 算法对比

React、Vue(及其底层的 Snabbdom)在 Diff 实现上有相似之处,也有关键差异:

  • 编译优化与静态标记 :React 的虚拟 DOM 纯粹运行时实现,每次渲染都会重新生成新树并全量遍历对比vuejs.org,无法跳过静态子树的 diff。相比之下,Vue(尤其是 Vue 3)在编译阶段就可标记静态节点和优化路径:静态子树(isStaticv-once)会被缓存复用,不在每次更新中重新 diffjonny-wei.github.iovuejs.org;编译器还会生成Patch Flags ,指示某些节点仅需要做特定更新,大幅减少不必要的检查和 DOM 操作vuejs.org。总之,Vue 利用编译时信息实现了"静态节点跳过"和目标渲染(targeted updates),而 React 则始终需要在运行时完全遍历新旧树。

  • 双端比较 vs 线性比较 :Vue 2 和 Snabbdom 采用经典的"双端指针"算法对比同层列表:同时维护旧节点数组和新节点数组的头尾指针,先尝试匹配头头、尾尾、头尾、尾头这四种情况,然后再使用 key 映射处理剩余节点jonny-wei.github.ioblog.csdn.net。这种方法可快速识别首尾节点的插入移动场景。React 官方文档则并未特别提及双端指针,而是默认顺序遍历。实际上,React 可以看作使用了简化的列表对比:无 key 时按索引直接比较,有 key 时用映射重排,但并没有显式做首尾双向匹配的优化。目前 React 的 diff 并未内置最长递增子序列(LIS)算法优化。Vue 3 在此基础上进一步引入了 LIS 优化:对于乱序的带 key 列表,会先计算新节点在旧列表中索引的最长递增子序列,只移动不在该序列内的节点,从而将复杂度从 O(n²) 降低到 O(n log n)blog.csdn.net。React 本身不使用 LIS,也无静态标记,其列表重排依赖简单的键值匹配逻辑。

  • 算法收敛性 :总体来看,React、Vue 以及 Snabbdom 的核心思路趋于一致:都是先做元素类型判断、保留同类型节点,依赖 key 保持子节点稳定,避免跨层遍历devpress.csdn.netblog.csdn.net。Snabbdom(Vue 2 的底层)和 React 都遵循 "O(n) 级别启发式"思想devpress.csdn.net。不同之处在于:Vue/Snabbdom 针对节点移动有更复杂的双端比对逻辑,并且 Vue 3 利用编译时信息(静态分析、Patch Flags)进行优化;而 React 则相对简单直接,依赖开发者提供 key 来保证最优更新。

综上,React 的 diff 算法通过"同层比较 + 键值匹配"实现高效更新,核心在于最大程度地复用旧树节点并最小化 DOM 操作legacy.reactjs.orglegacy.reactjs.org。Vue 3 在此基础上进一步利用编译器优化(静态标记、补丁标志)及 LIS 等算法,对典型场景做了专项优化jonny-wei.github.iovuejs.orgblog.csdn.net。Snabbdom 作为轻量级库,其双端 diff 算法与 React 思想类似,都优先匹配同层同 key 节点,只是实现细节略有差异blog.csdn.netdevpress.csdn.net

参考资料: React 官方《Reconciliation》文档legacy.reactjs.orglegacy.reactjs.org以及 Vue/Snabbdom 源码和社区分析jonny-wei.github.ioblog.csdn.netvuejs.orgdevpress.csdn.netblog.csdn.net等。上述内容综合了多方资料,以期完整揭示 React diff 算法的核心原理与细节。

相关推荐
Christo331 分钟前
TFS-2022《A Novel Data-Driven Approach to Autonomous Fuzzy Clustering》
人工智能·算法·机器学习·支持向量机·tfs
木木子999935 分钟前
超平面(Hyperplane)是什么?
算法·机器学习·支持向量机·超平面·hyperplane
生活不易,被迫卖艺2 小时前
Redux与React-环境准备(React快速上手1)
前端·javascript·react.js
♞沉寂2 小时前
数据结构——双向链表
数据结构·算法·链表
大阳1232 小时前
数据结构2.(双向链表,循环链表及内核链表)
c语言·开发语言·数据结构·学习·算法·链表·嵌入式
CUC-MenG3 小时前
2025牛客多校第六场 D.漂亮矩阵 K.最大gcd C.栈 L.最小括号串 个人题解
c语言·c++·算法·矩阵
2401_876221344 小时前
Tasks and Deadlines(Sorting and Searching)
c++·算法
天下无贼!4 小时前
【轮播图】H5端轮播图、横向滑动、划屏效果实现方案——Vue3+CSS position/CSS scroller
javascript·css·vue.js·vue
我要学习别拦我~4 小时前
逻辑回归建模核心知识点梳理:原理、假设、评估指标与实战建议
算法·机器学习·逻辑回归