前言
假设你有两棵各有1000个节点的树,传统树对比算法需要十亿级别的操作(O(n³))。那根本不可能用在浏览器里------一更新就死机。React团队发现,在实际Web应用中,树的变化符合一些规律,于是他们大胆做了3个假设,把复杂度降到了线性(O(n))。虽然有些场景会误判,但在99%的情况下,它准得吓人还快得离谱。
今天我们就来揭开这3个"神级假设",以及React是怎么基于它们对比DOM的。
一、3个假设:React的"赌注"
- 同层对比 :两个不同类型的元素会产生不同的树。
比如<div>变成<span>,React会直接销毁旧子树,重建新子树,不会浪费时间去比较子节点。 - 唯一标识 :开发者可以通过
key属性告诉React哪些子元素是稳定的。
比如列表顺序变化时,有key就能识别"这个li还是那个li",只是挪了个位置。 - 同级子节点只在该层比较 :不会跨层级移动节点。
如果某个节点从子节点变成了父节点的兄弟,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的做法:
- 旧第一个A,新第一个C:不同,更新A为C。
- 旧第二个B,新第二个A:不同,更新B为A。
- 旧第三个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会出问题了吧?