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

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

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

  • key 属性主要用在 Vue 的虚拟DOM算法,在新旧nodes对比时辨识VNodes
  • 如果不使用key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法
  • 而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除/销毁 key 不存在的元素

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

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

认识 VNode

我们先来解释一下 VNode 的概念:

  • 因为目前我们还没有比较完整的学习组件的概念,所以目前我们先理解 HTML 元素创建出来的 VNode;
  • VNode 的全称是 Virtual Node,也就是虚拟节点;
  • 事实上,无论是组件还是元素,它们最终在 Vue 中表示出来的都是一个个 VNode;
  • VNode 的本质是一个 JavaScript 的对象;

image-20210925134319170

虚拟 DOM

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

插入 F 的案例

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

image-20210925134420445

  • 我们可以确定的是,这次更新对于 ul 和 button 是不需要进行更新,需要更新的是我们 li 的列表:
    1. 在 Vue 中,对于相同父元素的子元素节点并不会重新渲染整个列表;
    2. 因为对于列表中 a、b、c、d 它们都是没有变化的;
    3. 在操作真实 DOM 的时候,我们只需要在中间插入一个 f 的 li 即可;
  • 那么 Vue 中对于列表的更新究竟是如何操作的呢?
    1. Vue 事实上会对于有 key 和没有 key 会调用两个不同的方法;
    2. 有 key,那么就使用 patchKeyedChildren 方法;
    3. 没有 key,那么久使用 patchUnkeyedChildren 方法;

Vue 源码对于 key 的判断

image-20210925134810853

没有 key 的操作(源码)

image-20210925134838494

没有 key 的过程如下

我们会发现上面的 diff 算法效率并不高:

  • c 和 d 来说它们事实上并不需要有任何的改动;
  • 但是因为我们的 c 被 f 所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增;

image-20210925134931707

有 key 执行操作(源码)

有 key 的 diff 算法如下

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

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

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

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

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

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

    所以我们可以发现,Vue 在进行 diff 算法的时候,会尽量利用我们的 key 来进行优化操作:

    • 在没有 key 的时候我们的效率是非常低效的;
    • 在进行插入或者重置顺序的时候,保持相同的 key 可以让 diff 算法更加的高效;