一、案例分析
<!DOCTYPE html> <html>
<head>
<title>key的作用及原理</title> </head>
<body>
<div id="demo">
<!-- <p v-for="item in items" :key="item">{{item}}</p> -->
<p v-for="item in items" :key="item">{{item}}</p> </div>
<script src="../../dist/vue.js"></script>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: { items: ['a', 'b', 'c', 'd', 'e'] },
mounted () {
// items: ['a', 'b', 'f', 'c', 'd', 'e']
setTimeout(() => {
this.items.splice(2, 0, 'f')
}, 2000);
},
});
</script>
</body>
</html>
上面案例重现的是以下过程 :
1. 不使用key的情况
在Vue内部执行更新时,会认为每个节点元素是相同,从而逐个深层次对比里面的元素进行更新操作。
也就是说,会两棵节点树的头部开始对比,若是不一致则进行更新操作,从第三个元素c开始做更新操作,替换成f,d替换成c,以此类推到遍历完成。总共进行了3次更新替换,1次新增操作。
2. 使用Key的情况
// 首次循环patch A A B C D E
A B F C D E
// 第2次循环patch B B C D E
B F C D E
// 第3次循环patch E C D E
F C D E
// 第4次循环patch D C D
F C D
// 第5次循环patch C C
F C
// oldCh全部处理结束,newCh中剩下的F,创建F并插入到C前面
在Vue的内部执行更新时,首先对两棵节点树的头部进行对比,发现前两个节点一致,第三个节点不一致;接着开始对比两棵节点树的尾部,发现后两个节点一致;所以在新节点树出现了新节点f;对应一致的节点不会进行更新操作,对于新节点f会进行新增插入操作。所以,在有key的情况下,Vue内部只进行一次新增插入操作。
二、源码分析
源码位置:src/core/vdom/patch.js
1. diff算法简析
diff算法是在Vue进行dom更新时使用的高效算法,也就是patchVnode()方法。
diff算法的整体策略是:深度优先,同层比较。
- 两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;
- 比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;
2. key的判断
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
由于在我们上面的案例中,都是p标签,如果不使用key,则执行sameVnode()都是会返回true。因为a.key = b.key = undefined,且下面的条件也都是true。所以在执行patch()时,从始至终都是进行“旧头新头”的判断,因为sameVnode()都是会返回true,从而降低了执行效率。
如果使用key,当key不同时,执行sameVnode()会返回false。会在两棵节点树的前两个元素a和b进行“旧头新头”判断后,从新节点树的第三个元素f开始,变成“旧尾旧头”的对比,最后才对多余的f进行dom操作。从而提高了效率。
三、结论
- key的作用主要是为了高效的更新虚拟DOM,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少DOM操作量,提高性能。
- 另外,若不设置key还可能在列表更新时引发一些隐蔽的bug。
- vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。