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