首先我们需要了解Diffing 算法
当对比两颗树时,Vue 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。
比对不同类型的元素
当根节点为不同类型的元素时,Vue 会拆卸原有的树并且建立起新的树。举个例子,当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成 <div> 都会触发一个完整的重建流程。
比如,当比对以下更变时:
<div>
<Counter />
</div>
<span>
<Counter />
</span>
Vue发现根节点不同(<div>变成了<span>),会放弃所有的旧节点,然后删除重新创建。
比对同一类型的元素
当比对两个相同类型的元素时,Vue 会保留 DOM 节点,仅比对及更新有改变的属性。比如:
<div class="before" title="stuff" />
<div class="after" title="stuff" />
通过比对这两个元素,Vue 知道只需要修改 DOM 元素上的 class 属性。
对子节点进行递归
在默认条件下,当递归 DOM 节点的子元素时,Vue 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation 进行更新。
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
Vue会对比两个元素的类型,然后判断元素上的key值有没有相关联对应的key值,由于没有加key,相关联对应的key也没有,就没法做对比,默认只能第一个和第一个对比,元素的类型是一样,但是元素的属性不一样,textContent 文本不同,所有更新了元素里面的文本textContent=‘Connecticut’ ,这样的暴力更新会导致DOM回流,所以非常消耗性能。
为了解决以上问题,Vue支持 key 属性。当子元素拥有 key 时,Vue 使用 key 来匹配原有树上的子元素以及最新树上的子元素。以下例子在新增 key 之后使得之前的低效转换变得高效:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
现在 Vue 知道只有带着 ‘2014’ key 的元素是新元素,带着 ‘2015’ 以及 ‘2016’ key 的元素仅仅移动了。
而且key值不建议使用遍历的时候的索引,如果你的列表涉及到排序或者列表需要增加和删除选项时会出现意想不到的问题。
<template>
<div class="hello">
<ul>
<li v-for="(item, index) in list" :key="index">
<input type="checkbox">
{{ item }}
</li>
</ul>
<button @click="handleClick">删除第一项</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data () {
return {
list: [1, 2, 3]
}
},
methods: {
handleClick () {
this.list.shift()
}
}
}
</script>
当选中第一项时
然后点击删除第一项
出现的问题是第二项被选中了,导致的原因就是Vue复用了DOM
在对比的过程中,第一项的key相同所以复用,属性不同更改只要节点里面的文本,第二项key相同所以复用,属性不同更改只要节点里面的文本,第三项key是没有了表示删除,所以实际点击删除第一项的按钮删除的是第三项的DOM内容。
那有人会问,明明选中和没选中DOM有不一样的地方,为什么还能复用,因为表单数据由 DOM 节点来处理(也就是DOM内部)。表单数据是没有由 Vue 组件来管理(比如使用v-model)。所以两个input上的属性是完全一样的,所以产生了复用第一个input。
下面的情况将不会复用删除的input
<template>
<div class="hello">
<ul>
<li v-for="(item, index) in list" :key="index">
<input type="checkbox" v-model="item.check">
{{ item.text }}
</li>
</ul>
<button @click="handleClick">删除第一项</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data () {
return {
list: [
{text: 'a', check: false},
{text: 'b', check: false},
{text: 'c', check: false},
]
}
},
methods: {
handleClick () {
this.list.shift()
}
}
}
</script>
所以key要使用唯一的ID
<template>
<div class="hello">
<ul>
<li v-for="item in list" :key="item.id">
<input type="checkbox">
{{ item.text }}
</li>
</ul>
<button @click="handleClick">删除第一项</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data () {
return {
list: [
{text: 'a', id: 1},
{text: 'b', id: 2},
{text: 'c', id: 3},
]
}
},
methods: {
handleClick () {
this.list.shift()
}
}
}
</script>
在diff的过程中首先对比第一个key,和他相关联的key没有找到,所以就删除节点,第二个key存在,并且属性啥的一样,就直接复用,第三个key也是。