前言
官方文档中写到 「建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。」
开发vue
的朋友也都知道,一定要去写上key
不然通过vue-cli
创建的项目会报出警告。也知道key是用来标记当前元素。这篇文章的目的是看一下源码,了解下我们为啥要去写这个key
虚拟dom回忆
之前写过关于虚拟dom执行的流程。我们了解到
- 如果
oldVnode
不是dom元素,并且和vnode
是sameVnode
会进行patchVnode
。 - 在
pathVnode
中,如果新老节点都有子节点并且不相等,会执行updateChildren
对比子节点,把子节点的差异更新到真实dom sameVnode
中会对比两个虚拟dom的key
和tag
从代码体会key的作用
下面的代码是没有加key的情况,在页面遍历展示arr数组,当点击按钮的时候往数组插入’X’。
<div id="app">
<button @click="handler">按钮</button>
<ul>
<li v-for="value in arr">{{value}}</li>
</ul>
</div>
<script src="../../dist/vue.js"></script> <script>
const vm = new Vue({
el: '#app',
data: {
arr: ['a', 'b', 'c', 'd']
},
methods: {
handler () {
this.arr.splice(1,0,'X')
}
}
})
</script>
下面点击按钮后会经历些什么
- 点击按钮后,
oldVnode
是:u
l,vnode
是ul
, 符合sameVnode
。进行patchVnode
- oldVnode的子节点遍历内容是:
['a', 'b', 'c', 'd']
, vnode的子节点遍历内容是:['a', 'X' ,'b', 'c', 'd']
, 新老节点都有子节点并且不相等。进入updateChildren
对比子节点 updateChildren
是diff算法的核心,我在文章的最后写了他的对比过程
总结
- 当没有设置 key 的时候
在updateChildren
中比较子节点的时候,会做三次更新 DOM 操作和一次插入 DOM 的操作 - 当设置 key 的时候
在updateChildren
中比较子节点的时候,因为oldVnode
的子节点的b,c,d
和newVnode
的x,b,c
的key
相同,所以只做比较,没有更新 DOM 的操作,当遍历完毕后,会再把 x 插入到 DOM 上DOM 操 作只有一次插入操作。
—————————————————————————————————华丽的分割线
updatechildren的对比过程
-
对比开始和结束节点的时候分四种
-
旧开始/新开始
如果旧开始和新开始是sameVnode(key和sel相同)
- 调用patchVnode()对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
如果不是sameVnode, 开始 旧结束/新结束比较
-
旧结束/新结束
如果旧结束和新结束是sameVnode(key和sel相同)
- 调用patchVnode()对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx-- / oldEndIdx–
如果不是, 开始 旧开始/新结束比较
-
旧开始/新结束
如果旧开始和新结束是sameVnode(key和sel相同)
-
调用patchVnode()对比和更新节点
-
把oldStartVnode 对应的dom元素,移动到右边,更新索引
如果不是, 开始 旧结束/新开始比较
-
-
旧结束/新开始
如果是sameVnode
- 调用patchVnode()对比和更新节点
- 把oldEndVnode 对应的dom元素,移动到左边,更新索引
-
如果以上四种都不满足
- 首先遍历新开始节点,在旧节点数组中依次查找是否有相同key值的节点,
- 如果没有,创建新dom元素插入到最前面的位置
- 如果找到了,并且判断sel属性是否相同,如果相同,旧节点会被复制给elmToMove,然后调用patchVNode对比节点,然后把elmToMove对应的dom节点移动到最前面
——————循环结束后
- 当老节点的所有子节点先遍历完成,说明新节点有剩余,把剩余节点批量插入到右边
- 新节点的所有子节点先遍历完成,说明老节点有剩余,把剩余老节点删除