# dom-diff原理
dom-diff指的是,将新老vdom,通过比对的方式,生成新的dom,或者更新老的dom属性信息。如果新老vnode是同一个vnode的话,则只需要更新属性即可。如果不是,则需要删除老的dom元素,然后创建新的dom并插入到页面中。
# diff流程
Vue中对dom-diff做了一些优化,它采用的移动头尾指针的方式来对比,主要流程如下:
- 如果老的头不存在(可能为undefined),跳过该元素,同时_老的头指针_右移;
- 如果老的尾不存在(可能为undefined),跳过该元素,同时_老的尾指针_前移;
- 老的头与新的头比较:如果是同一个vnode,则进行对比更新。如果不是,进行第2步;
- 老的尾与新的尾比较:如果是同一个vnode,则进行对比更新。如果不是,进行第3步;
- 老的头与新的尾比较:如果是同一个vnode,则进行对比更新。如果不是,进行第4步;
- 老的尾与新的头比较:如果是同一个vnode,则进行对比更新。如果不是,进行第5步;
- 如果不是以上情况,则从老的vnodes数组中,根据key去查找对应的vnode。
- 如果找到了,则直接移动该节点到当前_老的头指针_位置的前面,同时将老的oldVnode置为
undefined; - 如果找不到,则创建新的dom,并插入到_老的头指针_位置的前面;
- 如果找到了,则直接移动该节点到当前_老的头指针_位置的前面,同时将老的oldVnode置为
- 当整个对比完成后,如果新的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)
)
)
)
}
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。
我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

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

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

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

- 至此,整个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。
我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

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

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

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

# 3:元素倒序
假设老的vnodes数组为为A B C D,新的vnodes数组为D C B A,如下图所示。同时假设每个vnode的key与元素同名,分别为a b c d及 d c b a。
我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

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

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

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

# 4:删除元素
假设老的vnodes数组为为A B C D,新的vnodes数组为D C B,如下图所示。同时假设每个vnode的key与元素同名,分别为a b c d及 d c b。
我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

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

- 尾与头都是C元素,是同一个vnode,重复步骤1,直到D元素。
- 当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。
我们用黑色箭头表示开始指针,绿色箭头表示尾指针。

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

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

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

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

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

- 整个patch结束。