v-for中的key是什么作用?
-
在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个
key属性 -
这个key属性有什么作用呢?我们先来看一下官方的解释:
-
key属性主要用在Vue的
虚拟DOM算法,在新旧nodes对比时辨识VNodes; -
如果
不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法 -
而
使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;
-
官方的解释对于初学者来说并不好理解,比如下面的问题:
- 什么是
新旧nodes,什么是VNode? - 没有
key的时候,如何尝试修改和复用的? - 有
key的时候,如何基于key重新排列的?
- 什么是VNodes
VNode的全称是Virtual Node,也就是虚拟节点,事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode,VNode的本质是一个JavaScript的对象。
上面是我们写的html元素,下面是我们最终在Vue中表示的VNode节点。实际就是一个个对象,存在我们内存当中。最后在渲染我们真实dom。这样做最大的好处是可以跨平台性。
- 什么是虚拟 DOM 呢
如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree。
从上图可以看出,我们写在template 里面所有的html元素都会被vue转换成一个个VNode,最终,这些VNode会形成一个VNode Tree。也就是我们的虚拟DOM。
- 通过一个插入f案例。分析有key和没有key。前后的新旧虚拟DOM,Vue是怎么进行对比的。
我们先来看一个案例:这个案例是当我点击按钮时会在中间插入一个f;
当我们点击按钮button时,我们会在 ul里面插入一个li在b和c之间。
这次更新对于ul和button是不需要进行更新,需要更新的是我们li的列表:
1. 在Vue当中,相同父元素的子节点并不会重新渲染整个列表。
2. 因为对于ul列表中a、b、c、d它们是没有变化的。
3. 在操作真实dom的时候,我们只需在中间插入一个f的li即可。
那么Vue对于列表的更新是如果操作的呢?
1. Vue会对于有key和没有key调用两个不同的方法。
2. 有key,就会调用 patchKeyedChildren 方法。
3. 没有key 会调用 patchUnkeyedChildren
Vue对于有无 key 的判断。
通过上图我们可以清楚知道,有
key和无key,Vue调用的方法是不一样的。那这些方法都做了什么呢?
无key的情况。(看注释代码)
const patchUnkeyedChildren = (
c1: VNode[], // 旧的 VNode ['a','b','c','d']
c2: VNodeArrayChildren, // 新的 VNode ['a','b','f','c','d']
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length // 1 获取旧节点的长度 [a,b,c,d] 4
const newLength = c2.length // 2 获取新的节点的长度 [a,b,f,c,d] 5
// 3. 将两个长度进行比较,得到最小的长度值
const commonLength = Math.min(oldLength, newLength)
let i
// 4. 按照最小的长度进行遍历 commonLength = 4
for (i = 0; i < commonLength; i++) {
// 5. 将每一个新的节点 与 旧的节点进行 patch (我们这个案例遍历四次,a,b,c,d),patch 4次
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
// 6. patch 完毕后,再次比较两个长度大小。如果旧节点数大于新节点数,执行unmountChildren。反之执行mountChildren
if (oldLength > newLength) {
// 7 .移除剩余节点
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength
)
} else {
// 8. 创建新的节点
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
commonLength
)
}
}
我们会发现上面的diff算法效率并不高
- 因为c和d是没有被改变的。
- 但是因为我们的c被f所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增;
有key的情况,会调用patchKeyedChildren
总体分为5个步骤
第一步的操作是从头开始进行遍历、比较:
- a和b是一致的会继续进行比较;
- c和f因为key不一致,所以就会break跳出循环;
第二步的操作是从尾部开始进行遍历、比较:
第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:
第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:
第五步是最特色的情况,中间还有很多未知的或者乱序的节点:
总结:
- 1、Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作。
- 2、在没有key的时候我们的效率是非常低效的。
- 3、在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效。