vue3更新了diff算法中对比子节点的核心算法逻辑。性能综合提高约30%~50%。从算法思路上讲讲他们的区别,更多的细节不做讨论。个人理解篇,纯属个人理解脑洞思路整理,方便后续回忆。
diff算法解决的问题
vnode更新,diff算法寻求最佳性能同步更新真实dom。必须明确old vnode和old真实dom是过去时,new vnode对应的new真实dom是我们最终想要的。基于old,得到new,如果让性能最好?最简单粗暴就是把old干掉,直接生成new。但是性能消耗最大。最佳方法是,尽可能复用真实dom。对真实dom的操作消耗关系:更新属性不移动 < 更新属性后移动到对应位置 < 创建新节点插入到对应位置。所以,diff算法最终是要尽可能找到可被复用的真实dom,如果不要移动最好,否则移动到对应位置,最后创建没有可复用的新节点,删除多余的未被复用的老真实dom节点。 vue3的diff算法比vue2的快,主要原因是在于复用dom元素相同的情况,移动dom的次数减少,因为vue3用到了最长递增子序列方案。更细节的讨论放在后面分析。 所以算法目标是:
- 尽可能块的找到可复用真实dom节点
- 复用真实dom节点的时候,尽可能也复用其相对顺序,少做移动。
vue2的diff算法
vue2的diff算法采用了双端diff算法。
- 同时使用四个指针分别放到old子节点和new子节点的头和尾。
- 对比头头,尾尾,头尾,尾头,如果其一满足sameVnode,则进行真实dom复用且不需要移动真实dom,指针向内移动。否则,通过keyToOldIdxMap尝试快速找到old子节点中key相同sameVnode,如果找到,则复用真实dom,并移动到当前位置,然后将old子节点[i]设置为undefined,后续查找忽略此节点。如果没有找到则创建新的真实dom并插入。
- 最后满足old子节点的头尾交叉,或new子节点的头尾交叉。说明对比完了。此时如果old交叉,new未交叉,说明new子节点剩下的都是要新创建并插入。反之,old未交叉,new交叉,说明old子节点剩下的是多余的,需要从dom中移除。
vue2算法优劣分析
算法通过相互比较头尾,如果复用,不需要移动,直接复用。这个是优点也是缺点,缺点就在于只要复用的节点不在头尾,则一定得移动复用的真实dom。并且受头尾影响,假如头尾是两个新节点,就一直不能头尾复用,剩下的所有节点复用,只可能走keyToOldIdxMap的方法,在头尾直接寻找复用元素,并移动它。试想一下,如果头尾直接的元素都是可被复用的,并且更新前后的相对顺序未发生变化,则所有的移动几乎都是额外的性能开销。是否可以有其他方案,尽可能在复用old子节点的同时,也复用其相对顺序,把移动真实dom的次数降到最低,较少性能消耗。后面vue3的diff算法提供了一个解决此问题的更好思路。
vue3的diff算法
vue3的diff算法借鉴了字符串diff的一些思路。
- 首先进行预处理,将前置和后置可复用的节点找出,且是不需要移动的。
- 判断是否存在剩余节点。如果old子节点存在剩余节点,new子节点不存在,比对完成,移除多余的old子节点对应的真实dom。如果old子节点不存在剩余节点,new子节点存在,比对完成,新建多余的new子节点对应的真实dom并插入。如果old和new子节点都存在剩余子节点,则将剩余部分进行比对复用。
- 剩余部分比对非vue2的比对方式,用到了最长递增子序列,复用与最长递增子序列内的节点对应的old真实dom时,无需移动。进一步减少移动次数,提高了性能。
vue3算法优劣分析
该算法引入最长递增子序列,尽可能复用了原来的顺序,减少移动次数,提升了性能。发散一下,该算法是移动次数最少的方法吗?还有没有其他更好的算法思路,有待进一步研究。