考虑如下代码:
<template>
<div id="app">
<div>
<ul>
<li v-for="item in items" :key="item.id">{{ item.val }}</li>
</ul>
</div>
<button @click="change">change</button>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
items: [
{id: 0, val: 'A'},
{id: 1, val: 'B'},
{id: 2, val: 'C'},
{id: 3, val: 'D'}
]
}
},
methods: {
change() {
this.items.reverse().push({id: 4, val: 'E'})
}
}
}
</script>
change方法执行,Vue怎么更新DOM呢?是直接覆盖吗?
Vue内部更新DOM有一套自己的diff算法,通过对比新旧节点执行不同的逻辑
这里的新旧节点指的是根节点,比如上面的例子中
<div id="app">就是根节点,Vue会从根节点开始,从父到子依次遍历节点
首先需要对比新旧节点是否相同,步骤如下:
- 判断两个节点的
key是否相等,如果相同,执行下一步 - 判断两个节点的
tag是否相等,如果相同,执行下一步 - 遍历两个节点的的属性,判断属性是否相等,如果相同,执行下一步
- 如果到了这一步,则两个节点相同,否则两个节点不同
然后根据新旧节点是否相同执行不同的逻辑
新旧节点不同
如下两个节点:
graph TD; Root1-->A; Root1-->B; Root1-->C
graph TD; Root2-->A; Root2-->B; Root2-->C
Root1和Root2不同,即使他们的子节点相同
这种情况非常简单,直接把Root1替换成Root2即可
新旧节点相同
这个时候需要获取它们的children,然后分不同情况考虑
旧children不存在,新children存在
如下两个节点:
graph TD; Root
graph TD; Root-->A; Root-->B; Root-->C
只需把A、B、C批量插入到原有的Root下即可
旧children存在,新children不存在
如下两个节点:
graph TD; Root-->A; Root-->B; Root-->C
graph TD; Root
只需把A、B、C删除即可
新、旧children都存在
如下两个节点:
graph TD; Root-->A; Root-->B; Root-->C; Root-->D
graph TD; Root-->D; Root-->C; Root-->B; Root-->A; Root-->E
这种情况最复杂,Vue会通过双指针算法来更新children
第一步:
定义四个指针分别指向新旧children头尾
第二步:
分别对比
oldStartIndex和newStartIndex、oldEndIndex和newEndIndex、oldStartIndex和newEndIndex、oldEndIndex和newStartIndex是否相同(对比方法和上面的一样)
类似于这样:
对比过程中如果找到相同的节点,如果是start和end相同,就将旧的节点移到新节点的位置,如果是start和start或end和end相同,则不变
比如上面:找到D相同,将旧节点D移到新节点D的位置(在oldStartIndex对应的A之前插入D),然后将找到相同节点的指针往内部移动
对比过程中如果没有找到相同的节点,则将newStartIndex对应的元素在旧children中查找是否存在,如果存在,将旧的节点移到新节点的位置(在oldStartIndex之前插入),然后将找到相同节点的指针往内部移动
如果还没有找到相同的节点,则将newStartIndex对应的元素加入到旧children对应位置(在oldStartIndex之前插入),然后将newStartIndex指针往内部移动
第三步:
重复第二步的步骤,找到C相同,将旧节点C移到新节点C的位置,oldEndIndex指针和newStartIndex指针往内部移动
第四步:
重复第二步的步骤,找到B相同,将旧节点B移到新节点B的位置,oldEndIndex指针和newStartIndex指针往内部移动
第五步:
重复第二步的步骤,找到A相同,新旧A都在start位置,不用动,oldStartIndex指针和newStartIndex指针往内部移动
第六步:
重复第二步的步骤,oldStartIndex和newStartIndex、oldEndIndex和newEndIndex、oldStartIndex和newEndIndex、oldEndIndex和newStartIndex都不相同,则将newStartIndex对应元素E在旧children中查找是否存在,不存在,则将newStartIndex对应元素E加入到旧children对应位置(在oldStartIndex之前插入E)
总结
可以看到,diff的核心就是对比新旧节点,如果不同就替换,如果相同获取它们的children,根据不同情况做不同的更新逻辑,然后递归再对比children的children,这样就遍历整个DOM树完成diff