Vue2 Diff
Vue2 的 Diff 主要是采用双端 Diff 算法,通过四个指针(旧头、旧尾、新头、新尾)进行交叉对比,虽然能快速处理头尾相同的情况,但在节点顺序复杂变化时表现较差。当新旧节点的中间部分顺序不一致时,Vue 2 需遍历旧节点查找匹配项,导致时间复杂度升高到 O(n²)
示例场景
旧节点:A B C D E
新节点:C A D E G
Vue2 的处理流程:
查找与 C 相同的节点的位置,将 C 插入到 A 之前
头头相同,patch A 节点
查找与 D 相同的节点的位置,将 D 插入到 B 之前
新头旧尾相同,将 E 插入到 B 之前
查找与 G 相同的节点的位置,未找到,作为新节点插入到 B 之前
删除 B 节点
由于第一个循环就不满足头尾比较的任何一种匹配情况,所以复杂度为 O(n²) ,并且还做了 4 次 DOM
移动的操作。
Vue3 Diff
Vue3 细化了 Diff 节点比较的过程,并对示例这种复杂的情况作了一定的算法优化,我们结合源码来逐步分析 Vue3 Diff 的步骤。
从两端开始比较
此步骤可以用 O(n) 的复杂度将一些首尾相同的节点比较完毕,快速处理简单情况
从头开始比较
1 | let i = 0 // 起始索引 |
从尾开始比较
1 | // 2. 从尾部开始同步节点 |
其他情况
新节点还有,旧节点没了
如果双端比较完后,新节点还有多,老节点没了,那么,多出来的执行 mount 操作即可;
1 | // 3. 新节点还有,老节点没了 |
老节点还有,新节点没了
如果双端比较完后,老节点还有多,新节点没了,那么,多出来的执行 unmount 操作即可:
1 | // 4. 老节点还有,新节点没了 |
乱序情况
两端的节点比较完毕,剩下中间的节点是乱序的。如果两个乱序节点比较,如果直接暴力比较,在遍历一个数组的时候,再遍历另个数组,时间复杂度很高,为减少时间复杂度,这里使用空间换时间的做法,先将一个数组转为 map,在遍历一个数组的时候,直接在 map 中查询,时间复杂度就能减低很多。
1 | // 处于中间的乱序节点 |
遍历老节点,进行 patch 和 unmount 操作
1 | // 5.2 遍历剩余老节点,把能处理的处理了(patch, unmout) |
进行节点的移动和新增
乱序节点的复用和删除操作已经做完了,剩下的几点就是要移动或者新增的。如何移动效率最高,这里涉及到一个算法,最长递增子序列。
什么是“最长递增子序列”呢?举个例子,数组[0, 7, 8, 9, 3, 4, 5]
的最长递增子序列为[0, 7, 8, 9]
或[0, 3, 4, 5]
,即递增趋势的最长的子数组。最长递增子序列的解并不是唯一,但是我们只需要拿到其中一个解即可。
为什么需要获得最长递增子序列呢?因为只要找出最长的子序列,那么只要移动剩下的节点,就可以保证移动的次数最少。只要保证索引是递增的,这个子序列的相对位置就是不变的。
1 | // 5.3 移动和新建 |
最长递增子序列算法实现
1 | /** |
示例场景
我们再看同一个场景下,Vue 3 Diff 的步骤:
从两端开始比较,结果:
i = 0
e1 = 4
e2 = 4
将剩下未执行比较的新节点数组转
Map
:{'C' => 0, 'A' => 1, 'D' => 2, 'E' => 3, 'G' => 4}
遍历剩余老节点:
A 在
keyToNewIndexMap
可获取对应值1
,记录newIndexToOldIndexMap[1]
为0+1
,记录maxNewIndexSoFar
为 1,Patch AB 在
keyToNewIndexMap
获取不到对应值,删除节点 BC 在
keyToNewIndexMap
可获取对应值0
,记录newIndexToOldIndexMap[0]
为2+1
,由于newIndex < maxNewIndexSoFar
,记录moved = true
,Patch CD 在
keyToNewIndexMap
可获取对应值2
,记录newIndexToOldIndexMap[2]
为3+1
,记录maxNewIndexSoFar
为 2,Patch DE 在
keyToNewIndexMap
可获取对应值3
,记录newIndexToOldIndexMap[3]
为4+1
,记录maxNewIndexSoFar
为 3,Patch E获取最终的
newIndexToOldIndexMap
:[3, 1, 4, 5, 0]
获取最长递增子序列
[1, 2, 3]
,倒叙遍历newIndexToOldIndexMap
获取
newIndexToOldIndexMap[4]
,值为0
,证明是新增,新增节点 G,插入位置在最后获取
newIndexToOldIndexMap[3]
,值非0,证明是移动,下标3
在最长递增子序列中,表示改节点不需要移动获取
newIndexToOldIndexMap[2]
,值非0,证明是移动,下标2
在最长递增子序列中,表示改节点不需要移动获取
newIndexToOldIndexMap[1]
,值非0,证明是移动,下标1
在最长递增子序列中,表示改节点不需要移动获取
newIndexToOldIndexMap[0]
,值非0,证明是移动,下标0
不在最长递增子序列中,节点需要移动。将 C 移动到 A 之前。
可以看出,在Vue3 Diff处理的情况下,DOM
移动只做了一次,而且算法的复杂度为 O(nlogn),相较于 Vue2 有性能上的提升。
其他优化
除了算法上的优化,Vue3 还做了以下的性能提升点:
静态提升:Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用,这样就免去了重复的创建节点,优化了运行时候的内存占用。
事件监听缓存:默认情况下绑定事件的行为会被视为动态绑定,所以每次都会追踪它的变化,开启缓存后,没有了静态标记,下次diff时可以直接用。