react diff算法
前言
经常我们在各类文章中都看到react将diff的复杂度从O(n^3)优化为了O(n),那么传统的O(n^3)到底是如何得到的呢?react又是如何将其优化为了O(n)呢?
传统diff算法
传统的diff算法计算一棵树变成另一棵所需要的最少步骤的复杂度是O(n^3),如何得到?
- 旧数上的一个节点,它要跟新树上的所有节点对比(复杂度为O(n))
- 如果这个节点在新树上没有找到,那么这个节点将被删除,同时会从新树中遍历找几个节点去填补(复杂度增加到O(n^2))
- 旧树上的所有节点都会走这个过程(复杂度增加到O(n^3))
// 伪代码
const oldNodes = ['a', 'b', 'c', 'd', 'e'];
const newNodes = ['a', 'c', 'f', 'd', 'e'];
for (let i = 0; i< oldNodes.length; i++) { // 复杂度O(n)
for (let j = 0; j< newNodes.length; j++) { // 复杂度O(n^2)
// 旧节点在新节点中比对
if(未找到){
for (let k = 0; k< newNodes.length; k++) { // 复杂度O(n^3)
// 寻找填补项
}
}
}
}
有一篇论文专门讲述diff算法:diff论文
React diff
react diff 制定的三个策略
- Web UI中的DOM节点跨层级的移动特别少,可以忽略不计(tree diff);
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构(component diff);
- 对于同一层级的一组子节点,他们可以通过唯一的key进行区分(element diff);
tree diff
基于策略一,react对树的算法进行分层比较优化,两颗树只会对处在同一层级的节点进行比较。
按照策略一忽略了跨层级之间的移动操作,react tree diff只会对相同颜色内的DOM节点进行比对,当发现某一层级的节点已经不存在了,则该节点及其所有子节点都会被完全删除掉,不会进行进一步的比对,这样只需要对树遍历一次便能完成整个DOM树的比较,复杂度变为了O(n)。
根据上面的规则,在开发组件时,保持稳定的DOM结构会有助于性能的提升。例如,可以通过visibility属性控制元素的显示与隐藏,而不是display。
component diff
- 组件类型没有发生变化,继续比较Virtual DOM tree;
- 组件类型发生变化,则将该组件视为dirty component,并重新创建新的组件,包括所有的子节点,替换该组件;
- 对于同一类型的组件,有可能Virtual DOM并不会有任何变化,那么可以通过shouldComponentUpdate来判断组件是否需要进行diff;
如上图中组件D变更为组件G时,虽然他们的结构很相似,但是因为组件类型不同,react diff会重新创建一个新的组件G来替换整个组件D,并且E、F也会重新创建,不会复用组件D的。
element diff
当节点处于同一层级时,react提供了三种节点操作方式:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)、REMOVE_NODE(删除)。
- INSERT_MARKUP,新节点在不原集合中,需要对新节点执行插入操作;
- MOVE_EXISTING,新老集合中都有某一节点,但是所处位置不同,可以复用老节点,需要对节点进行移动操作;
- REMOVE_NODE,老集合中有节点在新集合中不存在,或者某一属性不一致,则需要执行删除操作;
针对element diff react提供了一个优化策略,同一层级的同组子节点,添加唯一key进行区分,这也是节点能否复用的前提。
无key示例
老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
有key示例
下面我们看看具体的diff流程
lastIndex表示访问过的节点在老集合中的最大索引位置,这是一个初始化为0的动态值。
- 遍历到B,B不做移动,lastindex = 1
- 遍历到E,发现旧集合中不存在,则创建E并放在新集合对应的位置,lastindex = 1
- 遍历到C,不满足index < lastindex,C不动,lastindex = 2
- 遍历到A,满足index < lastindex,A移动到对应位置,lastindex = 2
- 当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成
diff算法的不足
因为D节点在老集合里面的index 是最大的,使得A、B、C三个节点都会 index < lastindex,从而导致A、B、C都会去做移动操作。另外这只是极端情况,现实的场景中包含很多类似的情况。可以考虑倒着循环。
react diff算法的复杂度为什么是O(n)
原因在于React采用了三个策略,限制了很多情况,不会做过于复杂的计算。所有的比较都在同级进行,只需要一次循环就能完成所有的操作。
react diff的key值用数组的索引会有什么问题?
react 官方解释
我们强烈推荐,每次只要你构建动态列表的时候,都要指定一个合适的 key。如果你没有找到一个合适的 key,那么你就需要考虑重新整理你的数据结构了,这样才能有合适的 key。
如果你没有指定任何 key,React 会发出警告,并且会把数组的索引当作默认的 key。但是如果想要对列表进行重新排序、新增、删除操作时,把数组索引作为 key 是有问题的。显式地使用 key={i} 来指定 key 确实会消除警告,但是仍然和数组索引存在同样的问题,所以大多数情况下最好不要这么做。
会带来什么问题?
当用数组的下标作为key值时,会导致显示错位问题。在对数组进行删除,插入时,由于数组的下标是动态的,删除、插入最终都会体现在末尾元素。
正则原理剖析
闲人阅读 560
ESlint + Stylelint + VSCode自动格式化代码(2023)
谭光志赞 34阅读 20.7k评论 9
涨姿势了,有意思的气泡 Loading 效果
chokcoco赞 24阅读 2.2k评论 3
你可能不需要JS!CSS实现一个计时器
XboxYan赞 25阅读 1.7k评论 1
在前端使用 JS 进行分类汇总
边城赞 17阅读 2k
【代码鉴赏】简单优雅的JavaScript代码片段(一):异步控制
csRyan赞 26阅读 3.3k评论 1
「彻底弄懂」this全面解析
wuwhs赞 17阅读 2.4k
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。