# dom-diff原理

dom-diff指的是,将新老vdom,通过比对的方式,生成新的dom,或者更新老的dom属性信息。如果新老vnode是同一个vnode的话,则只需要更新属性即可。如果不是,则需要删除老的dom元素,然后创建新的dom并插入到页面中。

# diff流程

Vue中对dom-diff做了一些优化,它采用的移动头尾指针的方式来对比,主要流程如下:

  1. 如果老的头不存在(可能为undefined),跳过该元素,同时_老的头指针_右移;
  2. 如果老的尾不存在(可能为undefined),跳过该元素,同时_老的尾指针_前移;
  3. 老的头与新的头比较:如果是同一个vnode,则进行对比更新。如果不是,进行第2步;
  4. 老的尾与新的尾比较:如果是同一个vnode,则进行对比更新。如果不是,进行第3步;
  5. 老的头与新的尾比较:如果是同一个vnode,则进行对比更新。如果不是,进行第4步;
  6. 老的尾与新的头比较:如果是同一个vnode,则进行对比更新。如果不是,进行第5步;
  7. 如果不是以上情况,则从老的vnodes数组中,根据key去查找对应的vnode。
    1. 如果找到了,则直接移动该节点到当前_老的头指针_位置的前面,同时将老的oldVnode置为undefined;
    2. 如果找不到,则创建新的dom,并插入到_老的头指针_位置的前面;
  8. 当整个对比完成后,如果新的vnode还有元素,则根据虚拟vnode创建新元素并插入页面中。如果老的还有,则从页面中删除老的dom元素。


在Vue中,判断是否是同一个vnode时,内部调用的是sameVnode方法:

// 判断是否同一个vnode的依据主要是:tag及key
// 如果是同一个vnode,则复用dom元素,只需要更新属性
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (  // 同步组件
        a.tag === b.tag &&  // tag是否相同
        a.isComment === b.isComment &&  // 是否是注释类型
        isDef(a.data) === isDef(b.data) &&  // data不为undefined
        sameInputType(a, b)  // input类型是否相同
      ) || (  // 异步组件
        isTrue(a.isAsyncPlaceholder) &&  
        a.asyncFactory === b.asyncFactory &&  // asyncFactory是否相同
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们根据实际使用情况,分别对5种方式分析说明。

# 情况分析

# 1:尾部插入新元素

假设老的vnodes数组为为A B C D,新的vnodes数组为A B C D E F,如下图所示。同时假设每个vnode的key与元素同名,分别为a b c d及 a b c d e f。

我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

image.png

  1. 老的头与新的头对比,判断他们是否是同一个vnode,他们都是A。则更新老的vnode属性,同时新老指针同时右移,如下图:

image.png

  1. 开始第2轮比较,老的头与新的头都是B。重复第1步的操作,一直到D元素。

image.png

  1. 当D元素对比完成后。此时新老指针同时右移,我们会发现老的指针已经出界了。此时说明新的vnode数组中还有vnode。此时依次循环后续新的vnode,插入到当前老的尾指针(D)的后面。即依次循环E F,插入到D的后面。

image.png

  1. 至此,整个patch过程结束。

# 2:头部插入新元素

假设老的vnodes数组为为A B C D,新的vnodes数组为F E A B C D,如下图所示。同时假设每个vnode的key与元素同名,分别为a b c d及 f e a b c d。

我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

image.png

  1. 头与头比较,E和A不是同一个vnode。然后尾与尾比较,都是D。此时更新老的vnode属性,新老尾指针同时左移,结果如下:

image.png

  1. 开始第二轮比较,尾与尾比较,都是C。重复第1步操作,一直到A元素。

image.png

  1. 当A元素比较完成后,新老尾指针同时左移。此时我们发现老的尾指针已经出界了。而新的vnode数组中还有vnode。此时依次循环后续新的vnode,插入到当前老的尾指针(A)的前面。即依次循环F E,并插入老的尾指针(A)的前面。

image.png

# 3:元素倒序

假设老的vnodes数组为为A B C D,新的vnodes数组为D C B A,如下图所示。同时假设每个vnode的key与元素同名,分别为a b c d及 d c b a。

我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

image.png

  1. 老的头与新的头比较,A和D不是同一个vnode。老的尾与新的尾比较,D与A也不是同一个vnode。老的头与新的尾比较,发现都是A,更新老的A的属性,并将A移动到老的尾指针(D)的下一个兄弟节点的前面。同时老的头指针右移,新的尾指针左移,如下图所示。

image.png

  1. 头与头比,B和D不是同一个vnode。尾和尾比,D和B也不是同一个vnode。头和尾比,发现都是B,更新B的属性。同时将B移动到老的尾指针(D)的下一个兄弟节点(A)的前面,如下图所示:

image.png

  1. 以此重复上述动作,直到对比结束。

image.png

# 4:删除元素

假设老的vnodes数组为为A B C D,新的vnodes数组为D C B,如下图所示。同时假设每个vnode的key与元素同名,分别为a b c d及 d c b。

我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

image.png

  1. 尾与头都是D,同一个vnode。更新D属性,并将D插入到老的头指针(A)的前面。同时老的尾指针左移,新的头指针右移,如下图

image.png

  1. 尾与头都是C元素,是同一个vnode,重复步骤1,直到D元素。
  2. 当B元素对比完成后,此时新的开始头指针已经出界,对比结束。但是老的还剩下A,只需要删除A即可。

# 5: 乱序对比

假设老的vnodes数组为为A B C D,新的vnodes数组为E A F C N,如下图所示。同时假设每个vnode的key与元素同名,分别为a b c d及 e a f c n。

我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

image.png

  1. 头与头,尾与尾,头与尾,尾与头都不是同一个vnode。此时取出新的vnode的第一个元素E,根据key去老的vnodes中查找
    1. 此时,我们找不到E元素。则将E插入到老的头指针(A)的前面,并将新的头指针右移。

image.png

  1. 老的头A,与新的头A比较,发现是同一个vnode。因此,更新老的属性,同时新老头指针右移,如下图:


image.png

  1. 头与头,尾与尾,头与尾,尾与头都不是同一个vnode。当前元素为F,根据F的key去老的vnodes中查找:
    1. 此时,我们找不到F元素。则将F插入到老的头指针(B)的前面,并将新的头指针右移。

image.png

  1. 头与头,尾与尾,头与尾,尾与头都不是同一个vnode。当前元素为F,根据F的key去老的vnodes中查找:
    1. 此时,我们可以从老的vnodes中找到C。则将C插入到老的头指针(B)的前面,并将新的头指针右移。同时将老的vnodes中的C置为undefined(防止数组塌陷)。

image.png

  1. 头与头,尾与尾,头与尾,尾与头都不是同一个vnode。当前元素为N,根据F的key去老的vnodes中查找:
    1. 此时,我们找不到N元素。则将N插入到老的头指针(B)的前面,并将新的头指针右移。此时,新的头指针移动后会出界。老的vnodes中还有元素~~B C D~~。依次删除即可。

image.png

  1. 整个patch结束。