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)。

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

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

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

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

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

相关推荐
小林ixn22 分钟前
从 onclick 到 React 合成事件,再到完整应用逻辑:一次前端架构的深度剖析
前端·react.js·前端框架
Csvn1 小时前
🧩「找不到模块」排查全记录——Monorepo 下 TypeScript 路径别名的 5 种「不通」与根治方案
前端
腻害兔1 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
CodexDave1 小时前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡
Ai_easygo2 小时前
AI Agent开发入门——从ReAct到Tool Calling,拆解Agent的底层运行逻辑
前端·人工智能·react.js
研☆香2 小时前
分析制作html页面,如何划分页面结构
前端
腻害兔3 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:支付模块 yudao-module-pay,一个让产品经理都看懂的支付中台设计
java·前端·vue.js·产品经理·ai编程
kyriewen3 小时前
我扒了最近的前端面经——2026年面试不背八股文了,考这5样
前端·面试·ai编程
IT_陈寒3 小时前
Redis的持久化配置把我坑惨了:你以为数据安全了?
前端·人工智能·后端
小徐_23333 小时前
uni-app 项目别再从零搭了!3 个 Wot UI 起手模板怎么选?
前端·uni-app