之前一直看diff算法的 新前/旧前什么的感觉一脸懵,不知道那玩意是干啥的。今天整理了下。来说说我的理解,如果说的不对,欢迎大佬们指点~
话不多说直接上手。 本文讲解虚拟dom以 snabbdom 为例子讲解
新前/新后/旧前/旧后 本质上来说就是两个虚拟dom上的开始和结束节点。那么对比自然也是四个节点之间的对比了。
在讲解对比规则之前,我们先来大致了解下patchVnode方法,也就是对比两个虚拟dom,并且更新真实dom。(讲解的 patchVnode会跳过钩子函数。)
注意:isDef 判断的是不等于 undefined。 isUndef 判断的是等于 undefined
patchVnode源码地址
对比步骤大致可以分
- 对比两个虚拟dom是否为同一个对象,如果是那么就说明当前虚拟dom并没有更改,所以直接return
if (oldVnode === vnode) return
- 判断新的虚拟dom是否有text属性,如果新的虚拟dom有text属性,并且新虚拟dom text属性与旧的虚拟dom text属性不相等,那么就直接替换旧的虚拟dom text属性
- 如果新的节点没有text属性 (注意:text属性与children属性只可存在一个。具体可以看看 h函数的重载)
- 判断新的虚拟dom和旧的虚拟dom是否有children,并且是否相等,如果不相等,则进行深层次的diff对比
if (isDef(oldCh) && isDef(ch)) { // updateChildren 就是diff对比两个节点内容 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) }
- 如果旧的虚拟dom没有children,新的虚拟dom有children,那么我们需要去判断旧的虚拟dom是否有text属性,如果有则直接清空即可。然后把新的children节点添加到旧的children上。也就是挂载到真实的dom上。
// 如果新的节点有children,因为上面判断过 如果 旧的children和新的children都有的情况,所以这里只需要判断一个是否存在,另一个则一定不存在 else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) }
- 如果旧的虚拟dom上有children,新的虚拟dom没有children,则直接清空旧的虚拟dom的children即可,也就是删除挂载的dom的children节点
else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) }
- 最后如果新旧虚拟dom上都没有children,并且新的虚拟dom上也没有text属性,则直接判断旧的虚拟dom上是否有text属性,如果有直接清空即可
else if (isDef(oldVnode.text)) { api.setTextContent(elm, '') }
diff对比 源码地址
首先我们先建立四个节点和四个节点所对应的索引
对应的源码八个变量
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
对比规则
-
旧前与新前对比(也就是两个虚拟dom的开始节点对比)
if (sameVnode(oldStartVnode, newStartVnode)) { // 更新dom patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); // 指针下移 oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; }
-
旧后与新后对比 (两个结束节点的对比)
else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); // 指针上移 oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; }
-
旧后与新前对比 (旧的未处理的最后一个节点与新的未处理的第一个节点的对比)
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore( parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!) ); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; }
旧后与新前对比完成之后如果符合条件,那么我们除了需要进行patchVnode对比更新dom之外,还需要把旧后的真实dom节点插入到旧前的前面一个节点,最后再让旧后的指针上移,新前的指针下移
- 为什么要把旧后移动到旧前的前面呢?
-
如果旧后与新前匹配上了,则意味着是旧后移动了,移动到了新前的这个位置。而新前的前一个节点与旧前的前一个节点必定是相同的节点。所以这个时候我们把旧后移动到旧前的前一个节点,这样移动完的节点位置就与新前在同一个位置了。
-
节点移动之后
-
- 为什么要把旧后移动到旧前的前面呢?
-
旧前与新后对比(旧的未处理的第一个节点与新的未处理的最后一个节点的对比)
else if (sameVnode(oldEndVnode, newStartVnode)) {
// 更新dom
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
// 将旧前移动到旧后的后面
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
旧前与新后对比完成之后如果符合条件,那么我们除了需要进行patchVnode对比更新dom之外,还需要把旧前的真实dom节点插入到旧后的后面一个节点,最后再让旧前的指针下移,新后的指针上移
- 为什么要把旧前移动到旧后的后面呢?
- 如果旧前与新后匹配上了,则意味着是旧前移动了,移动到了新后的这个位置。而新后的后一个节点与旧后的后一个节点必定是相同的节点。所以这个时候我们把旧前移动到旧后的后一个节点,这样移动完的节点位置就与新后在同一个位置了。
- 节点移动之后
-
如果这四个规则都匹配不上,那么就需要遍历寻找了。而 snabbdom 遍历是做了缓存处理的。就是缓存当前所对应的旧前/旧后之间的 key 与 索引的关系 源码位置
-
缓存之后。就去这个缓存的数组中找新前对应的key所对应的索引。如果找到了:
- 则意味着当前节点是移动了节点,那么会进行patchVnode,并且将旧的虚拟dom中的那个节点移动到旧前的前面
- 为什么是移动到旧前的前面呢?因为旧前的前面一个节点与新前的前面一个节点是相同的,所以需要把找到的这个节点移动到旧前的前面。
- 最后再把找到的这个节点设置为undefined。这样在while开始的时候就需要判断当前找到的新前/旧前/新后/旧后是否为undefined了,如果是undefined,那么就需要将指针下/上移跳过当前undefined节点
- 最后把新前指针下移。而旧前/旧后指针是不需要移动的
- 移动之后的节点
-
如果说找不到:
- 则说明当前节点是新增的节点,那么就需要为当前节点创建dom,并且挂载到旧前的前面。
if (isUndef(idxInOld)) { // New element api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm! ); }
-
-
结束循环之后还需要做处理。因为这个时候可能新的虚拟dom先处理完成但是旧的虚拟dom没有处理完成,也可能是旧的虚拟dom先处理完成但是新的虚拟dom没有处理完成
- 旧的虚拟dom还有没有处理完成的节点
- 旧的虚拟dom中还有没有处理完成的节点就意味着剩余的旧的节点中的节点都是需要删除的。因为他在新的虚拟dom中已经不存在了,所以这个时候我们遍历删除即可 (注意一个小知识:在for循环中。++i 与 i++ 的执行结果是一致的)removeVnodes源码
if (oldStartIdx <= oldEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); }
- 新的虚拟dom节点还有没处理完的节点
- 新的虚拟dom中还有没有处理完成的节点就意味着剩余的新的节点都是新增的,因为它在旧的虚拟dom中是找不到的,而这个时候旧的虚拟dom已经遍历完成了,所以剩下的都是新增的。这个时候就需要把剩余的未处理的节点插入到新后的后面
- 为什么是插入到新后的后面呢?因为新后的后面一定是已经处理完之后的节点,在旧的虚拟dom中也是对应着相同的位置。所以需要把新增的节点插入到新后的后面。
- 新后后面如果没有节点,则会自动插入到最后一个节点的下面。也就是相当于是 appendChildren
- 为什么是插入到新后的后面呢?因为新后的后面一定是已经处理完之后的节点,在旧的虚拟dom中也是对应着相同的位置。所以需要把新增的节点插入到新后的后面。
- 新的虚拟dom中还有没有处理完成的节点就意味着剩余的新的节点都是新增的,因为它在旧的虚拟dom中是找不到的,而这个时候旧的虚拟dom已经遍历完成了,所以剩下的都是新增的。这个时候就需要把剩余的未处理的节点插入到新后的后面
if (newStartIdx <= newEndIdx) { // 这个before就是新后的后一个节点。如果是null,则会插入到最后一个 before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes( parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); }
- 旧的虚拟dom还有没有处理完成的节点