React Diff算法:3个“神级假设”让虚拟DOM快得像闪电
假设你有两棵各有1000个节点的树,传统树对比算法需要十亿级别的操作(O(n³))。那根本不可能用在浏览器里——一更新就死机。React团队发现,在实际Web应用中,树的变化符合一些规律,于是他们大胆做了3个假设,把复杂度降到了线性(O(n))。虽然有些场景会误判,但在99%的情况下,它准得吓人还快得离谱。 今天我们就来揭开这3个“神级假设”,以及React是怎么基于它们对比DOM的。 基于这些假设,React设计出了基于广度优先遍历的Diff算法。 如果旧树是 即使 所以尽量保持DOM类型稳定,比如别把 如果新旧节点类型相同(比如都是 React保留 这时子节点的对比就进入“列表对比”阶段。 这是Diff最精彩的部分。 假设子节点都是同一类型,但顺序变化。没有key,React只能逐个比较位置。 React的做法: 给每个子节点加唯一key,React就能追踪节点的身份。 React会构建一个“旧节点键值映射”,然后遍历新列表: 注意:千万不要用 由于第3个假设“不同层级不比较移动”,如果你把一个子节点从父节点内移动到另一个父节点下,React会直接卸载重建,而不是复用。 React会把 整个Diff过程是递归的:从根开始,深度优先遍历,同级对比子节点。由于假设了同层对比,整个递归树的大小就是原树的大小,复杂度O(n)。 配合 这三条简单规则,让React在大多数场景下既快又准。理解Diff,你就能写出更高效的组件:给列表加稳定key,避免不必要的DOM类型改变,用 现在你知道为什么map时要加key,为什么不能随意把div改成span,为什么index做key会出问题了吧?前言
一、3个假设:React的“赌注”
比如 <div> 变成 <span>,React会直接销毁旧子树,重建新子树,不会浪费时间去比较子节点。key 属性告诉React哪些子元素是稳定的。
比如列表顺序变化时,有key就能识别“这个li还是那个li”,只是挪了个位置。
如果某个节点从子节点变成了父节点的兄弟,React会销毁重建,而不是复用。二、节点类型不同:直接“拆房重建”
<div>,新树是 <span>,React压根不看子节点,直接删掉旧节点及其所有子节点,重新创建 <span> 及子节点。// 旧
<div><Counter /></div>
// 新
<span><Counter /></span><Counter /> 是一样的,整个组件也会被卸载再重新挂载,Counter 的state会丢失,生命周期重新走一遍。<div> 随意改成 <section>。三、同一类型节点:保留DOM,只更新属性和子节点
<div>),React会保留该节点的DOM元素,然后对比属性,更新改变的属性。接着递归对比子节点。// 旧:<div className="old" title="tip">hello</div>
// 新:<div className="new" title="tip">world</div><div>,把 className 从 "old" 改为 "new",然后对比文本子节点,把 "hello" 改成 "world"。四、列表对比:没有key VS 有key
没有key时:React的“暴力”
// 旧:A - B - C
// 新:C - A - B
最终结果正确,但进行了3次更新操作。实际上只需要把C移到最前面就能复用A、B。这就是没有key的低效。有key时:移动、插入、删除三步走
// 旧:key=A - key=B - key=C
// 新:key=C - key=A - key=B
最后React只做一次移动操作(将C移到最前),其余复用。性能大大提升。index 作为key!因为列表顺序变化时,index也会变,React会误判,导致性能退化和组件状态错乱。五、跨层级移动:React无能为力
// 旧
<div>
<span>hello</span>
</div>
// 新
<span>hello</span><span> 从 <div> 下删掉,再重新创建到新位置。虽然有点浪费,但这样可以保持算法简单快速。六、递归Diff与性能优化
shouldComponentUpdate、React.memo 可以跳过整棵子树的Diff,进一步提升性能。七、总结:Diff算法的“三板斧”
memo 跳过无意义的更新。