Diff,顾名思义,different,找不同。Vue里用来对比虚拟DOM,更新页面内容。
1. 传统Diff
循环递归,对节点进行比较,判断每个节点的状态和增删改的操作。
graph TD
拿到新旧节点 --> 初始化全局变量result存储子节点操作类型 --> 比较该子节点的长度 --> 根据较长的子节点开始循环遍历 --> 一些对比规则 --> Stop
在此基础上,又升级到了新的Diff算法,比传统Diff算法更简单。
2. React-提出了更高效的Diff算法
React开发者大胆提出后,难度从传统的O(n^3)降低到O(n)。
假设1:两个相同组件产生类似的DOM结构,不同组件产生不同的DOM结构
假设2:对于同一层级的一组子节点,它们可以通过唯一的id来区分
基于上面2种假设,那么相同结构的DOM,只需要进行同层级的比较就行。
- 不同类型的节点比较
如果新旧节点的类型不同,那么直接删除旧节点和其子节点并插入新节点,因为我们前面设定了相同组件DOM结构相同,如果不同则当机立断插入新的,不浪费时间去比较。 - 相同类型的节点比较
Diff算法重新设置该节点的属性,实现节点的更新。 - 列表节点的比较
节点通常操作有增加、删除、排序,添加一个key会更高效。
3. Vue的Diff算法
Vue的Diff与React提出的相似,也是同级比较,类型不同则插入新的,类型相同则更新属性,最后删除新节点列表中不包含的旧节点。源码位于src/core/vdome/patch.js-updateChildren方法。
看上图,已知旧节点“ABCD”,新节点“ECFA”,其中,旧节点的开始和结束下标分别记录,新节点的也记录。那么,我们开始Diff对比:
首先,非空判断:old_start_index<=old_end_index&&newStartIndex<=newEndIndex。
然后开始正式对比。
- 旧节点的首节点和末节点是否为空。
if(isUndef(old_start_vNode)){
//如果首节点是空的,则后移,判断下一个
oldStartVnode = oldch[ ++oldStartIndex ]
}
else if(isUndef(old_end_vNode)){
//如果末节点是空的,则前移,继续判断前一个
oldEndVnode = oldch[ --oldEndIndex ]
}
- 判断首节点和末节点是否相同
//sameVnode方法:判断节点是否相同,包括key、tag、inComment等属性的是否相等。
else if( sameVnode(oldStartVnode, newStartVnode ) ){
//相同,根据上面的分析,更新属性即可更新节点内容
patchVnode( oldStartVnode, newStartVnode, insertVnodeQueue )
//结束这个节点的判断,更新哨兵位置
oldStartVnode = oldch[ ++oldStartIndex ]
newStartVnode = newch[ ++newStartIndex ]
}
else if( oldEndVnode, newEndVnode ){
patchVnode( oldStartVnode, newStartVnode, insertVnodeQueue )
//更新哨兵位置
oldEndVnode = oldch[ --oldEndIndex ]
newEndVnode = newch[ --newEndIndex ]
}
- 列表的判断,用sameVnode判断旧的头和新的尾
else if(sameVnode(oldStartVnode, newEndVnode)){
patchVnode( oldStartVnode, newStartVnode, insertVnodeQueue )
//真实DOM移动到真实节点列表的最后面
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSiblinng(oldEndVnode.elm))
oldStartVnode = oldch[ ++oldStartIndex ]
newEndVnode = newch[ --newEndIndex ]
}
else if(sameVnode(oldEndVnode, newStartVnode)){
patchVnode( oldEndVnode, newStartVnode, insertVnodeQueue )
//真实DOM移动到真实节点列表的最后面
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSiblinng(oldEndVnode.elm))
oldEndVnode = oldch[ --oldEndIndex ]
newStartVnode = newch[ ++newStartIndex ]
}
- 如果上面都不满足,进入最后一个判断
直到循环结束。
最后旧节点中剩下的是要删除的,新的中是要新增的。
“就地复用”策略
Diff中有个就地复用策略,就是Vue会尽可能复用之前的DOM,尽量不发生DOM的移动。
Vue判断新旧节点是否相同(sameVnode),实际上这个操作仅判断是否为同类节点(同类节点:类型相同、节点数据一致,如2个span,虽然文本不同也算同类节点),判断出的同类节点,Vue会直接复用DOM,只修改其中的文本。这样大大减少了列表中的节点移动操作。
总结
Vue中的Diff与React中的思路类似,都是同层节点比较,用了些优先判断规则和就地复用,提高了Diff算法效率。
关于diff算法,面试中如何组织语言?
- 作用:修改一小段DOM,不会引起DOM树的重绘。
- 实现原理:将虚拟DOM的某个节点的数据改变后生成新的DOM节点,然后与旧节点进行比较,并替换为新节点。具体就是调用patch方法对比新旧节点,以便比较一边给真实DOM打补丁进行替换。
- (patch:Vue 的核心函数之一,实现起来比较复杂,即使Vue团队采用了优化算法。用于将虚拟DOM转为真实DOM。每次数据变化,patch会把新虚拟DOM和旧真实DOM比价,然后更新到真实DOM)
- 简单说明Diff算法的过程: 1.同级比较,再比较子节点。 如果节点类型不同,则直接删除旧节点,插入新节点,不再比较此节点的子节点。如果新DOM没有子节点,则直接删除旧子节点。2.核心算法:类型相同且有子节点,则递归比较子节点。Vue2的diff采用了双端比较,同时从新旧children的首尾两端开始比较,用key值定位到可复用的节点。如果新旧children只有顺序不同,最佳操作是移动元素,需要在新旧children中保持映射关系,以便找到可复用节点。相比React的DIff,相同情况可以减少节点的移动次数,更优雅。3.Vue3借鉴了ivi和inferno算法,在创建Vnode时定义其类型,在mount/patch时用位运算来判断一个Vnode的类型,在此基础上再配合核心Diff算法,性能比Vue2更提升。还运用了动态规划的思想求解最长递归子序列。