React Diff算法:3个“神级假设”让虚拟DOM快得像闪电

前言

假设你有两棵各有1000个节点的树,传统树对比算法需要十亿级别的操作(O(n³))。那根本不可能用在浏览器里------一更新就死机。React团队发现,在实际Web应用中,树的变化符合一些规律,于是他们大胆做了3个假设,把复杂度降到了线性(O(n))。虽然有些场景会误判,但在99%的情况下,它准得吓人还快得离谱。

今天我们就来揭开这3个"神级假设",以及React是怎么基于它们对比DOM的。

一、3个假设:React的"赌注"

  1. 同层对比 :两个不同类型的元素会产生不同的树。
    比如 <div> 变成 <span>,React会直接销毁旧子树,重建新子树,不会浪费时间去比较子节点。
  2. 唯一标识 :开发者可以通过 key 属性告诉React哪些子元素是稳定的。
    比如列表顺序变化时,有key就能识别"这个li还是那个li",只是挪了个位置。
  3. 同级子节点只在该层比较 :不会跨层级移动节点。
    如果某个节点从子节点变成了父节点的兄弟,React会销毁重建,而不是复用。

基于这些假设,React设计出了基于广度优先遍历的Diff算法。

二、节点类型不同:直接"拆房重建"

如果旧树是 <div>,新树是 <span>,React压根不看子节点,直接删掉旧节点及其所有子节点,重新创建 <span> 及子节点。

jsx 复制代码
// 旧
<div><Counter /></div>
// 新
<span><Counter /></span>

即使 <Counter /> 是一样的,整个组件也会被卸载再重新挂载,Counter 的state会丢失,生命周期重新走一遍。

所以尽量保持DOM类型稳定,比如别把 <div> 随意改成 <section>。

三、同一类型节点:保留DOM,只更新属性和子节点

如果新旧节点类型相同(比如都是 <div>),React会保留该节点的DOM元素,然后对比属性,更新改变的属性。接着递归对比子节点。

jsx 复制代码
// 旧:<div className="old" title="tip">hello</div>
// 新:<div className="new" title="tip">world</div>

React保留 <div>,把 className 从 "old" 改为 "new",然后对比文本子节点,把 "hello" 改成 "world"。

这时子节点的对比就进入"列表对比"阶段。

四、列表对比:没有key VS 有key

这是Diff最精彩的部分。

没有key时:React的"暴力"

假设子节点都是同一类型,但顺序变化。没有key,React只能逐个比较位置。

jsx 复制代码
// 旧:A - B - C
// 新:C - A - B

React的做法:

  1. 旧第一个A,新第一个C:不同,更新A为C。
  2. 旧第二个B,新第二个A:不同,更新B为A。
  3. 旧第三个C,新第三个B:不同,更新C为B。 最终结果正确,但进行了3次更新操作。实际上只需要把C移到最前面就能复用A、B。这就是没有key的低效。

有key时:移动、插入、删除三步走

给每个子节点加唯一key,React就能追踪节点的身份。

jsx 复制代码
// 旧:key=A - key=B - key=C
// 新:key=C - key=A - key=B

React会构建一个"旧节点键值映射",然后遍历新列表:

  • 新第一个C,在旧里有,且位置变了,标记为"移动"。
  • 新第二个A,旧里有,标记为"移动"。
  • 新第三个B,旧里有,标记为"移动"。 最后React只做一次移动操作(将C移到最前),其余复用。性能大大提升。

注意 :千万不要用 index 作为key!因为列表顺序变化时,index也会变,React会误判,导致性能退化和组件状态错乱。

五、跨层级移动:React无能为力

由于第3个假设"不同层级不比较移动",如果你把一个子节点从父节点内移动到另一个父节点下,React会直接卸载重建,而不是复用。

jsx 复制代码
// 旧
<div>
  <span>hello</span>
</div>
// 新
<span>hello</span>

React会把 <span> 从 <div> 下删掉,再重新创建到新位置。虽然有点浪费,但这样可以保持算法简单快速。

六、递归Diff与性能优化

整个Diff过程是递归的:从根开始,深度优先遍历,同级对比子节点。由于假设了同层对比,整个递归树的大小就是原树的大小,复杂度O(n)。

配合 shouldComponentUpdate、React.memo 可以跳过整棵子树的Diff,进一步提升性能。

七、总结:Diff算法的"三板斧"

  • 类型不同:删了重建。
  • 类型相同:保留DOM,更新属性和子节点。
  • 子节点列表:靠key识别身份,移动/增删。

这三条简单规则,让React在大多数场景下既快又准。理解Diff,你就能写出更高效的组件:给列表加稳定key,避免不必要的DOM类型改变,用 memo 跳过无意义的更新。

现在你知道为什么map时要加key,为什么不能随意把div改成span,为什么index做key会出问题了吧?

相关推荐
j7~5 分钟前
IT 面试指南:从准备到 Offer 的完整路线图(校招 / 实习通用
面试·职场和发展·秋招·春招·实习面试·it后端
Amos_Web10 分钟前
Rspack 源码解析(三十一):HotModuleReplacementPlugin 如何用 hash 比对生成增量更新
前端
尤水就下21 分钟前
🔥说一说在线文档的 Block 架构是如何实现的
前端·javascript·架构
小淮AI1 小时前
面试前 48 小时,怎样练表达和复盘:一份个人准备记录
面试·职场和发展
是江迪呀1 小时前
介绍一个开源项目 —— 烂笔头
前端·后端
恋猫de小郭1 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
紧跟先前的步伐1 小时前
Go AI Agent 实战 18|Tool Calling 的基本原理与完整执行流程
前端·数据库·人工智能·ai·golang·1024程序员节
考研保研资料分享1 小时前
双非转专业保研海军军医大学心理学:四校考核、学年论文与择校复盘
面试
captain3761 小时前
Spring Web MVC(2)
java·前端·spring boot·后端·spring·mvc
晓得迷路了1 小时前
栗子前端技术周刊第 151 期 - ViteConf 2026、React Router 8.5、React Native 0.88...
前端·javascript·vue.js