Vue中key的理解。

289 阅读4分钟

v-for中的key是什么作用?

  • 在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性

  • 这个key属性有什么作用呢?我们先来看一下官方的解释:

    1. key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;

    2. 如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法

    3. 使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;

官方的解释对于初学者来说并不好理解,比如下面的问题:

  1. 什么是新旧nodes,什么是VNode
  2. 没有key的时候,如何尝试修改和复用的?
  3. key的时候,如何基于key重新排列的?
  1. 什么是VNodes

VNode的全称是Virtual Node,也就是虚拟节点,事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode,VNode的本质是一个JavaScript的对象

image.png

上面是我们写的html元素,下面是我们最终在Vue中表示的VNode节点。实际就是一个个对象,存在我们内存当中。最后在渲染我们真实dom。这样做最大的好处是可以跨平台性

  1. 什么是虚拟 DOM 呢

如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree

image.png

从上图可以看出,我们写在template 里面所有的html元素都会被vue转换成一个个VNode,最终,这些VNode会形成一个VNode Tree。也就是我们的虚拟DOM

  1. 通过一个插入f案例。分析有key和没有key。前后的新旧虚拟DOM,Vue是怎么进行对比的。

我们先来看一个案例:这个案例是当我点击按钮时会在中间插入一个f;

image.png

当我们点击按钮button时,我们会在 ul里面插入一个lib和c之间。

这次更新对于ul和button是不需要进行更新,需要更新的是我们li的列表:

1. 在Vue当中,相同父元素的子节点并不会重新渲染整个列表。
2. 因为对于ul列表中ab、c、d它们是没有变化的。
3. 在操作真实dom的时候,我们只需在中间插入一个f的li即可。

那么Vue对于列表的更新是如果操作的呢?

1. Vue会对于有key和没有key调用两个不同的方法。
2. 有key,就会调用 patchKeyedChildren 方法。
3. 没有key 会调用 patchUnkeyedChildren

Vue对于有无 key 的判断。

image.png

通过上图我们可以清楚知道,有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所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增;

image.png

key的情况,会调用patchKeyedChildren

总体分为5个步骤

image.png

第一步的操作是从头开始进行遍历、比较:

  • a和b是一致的会继续进行比较;
  • c和f因为key不一致,所以就会break跳出循环;

image.png

第二步的操作是从尾部开始进行遍历、比较:

image.png

第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:

image.png

第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:

image.png

第五步是最特色的情况,中间还有很多未知的或者乱序的节点:

image.png

总结:

  • 1、Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作。
  • 2、在没有key的时候我们的效率是非常低效的。
  • 3、在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效。