likes
comments
collection
share

vue中别再使用index做key啦!

作者站长头像
站长
· 阅读数 15

前言

既然我们这里提到了key,那么我们就不得不来简单的介绍一下key的作用了:

在 Vue 中,key 是用于优化列表渲染的一个特殊属性。当你使用 v-for 指令来渲染一个列表时,为每个列表项提供一个唯一的 key 是很重要的。这是因为 Vue 通过 key 来识别节点,并决定是否复用已存在的 DOM 元素,以及确定如何高效地更新 DOM。

key 应该是唯一且稳定的,这样 Vue 才能正确地识别和管理每个节点。

在文章中我们还会对key的作用详细解释。

虚拟DOM

首先,我需要先来给大家介绍一下虚拟DOM。虚拟 DOM 可以帮助减少直接 DOM 操作的次数,从而提高性能。首先假设我们有这样一个情景:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <ul class="list" id="list">
      <li class="item" v-for="item in list">{{item}}</li>
    </ul>
    
  </div>

  <script>
    const { createApp, ref } = Vue

    createApp({
      setup() {

        const list = ref(['html', 'css', 'vue'])

        return {
          list
        }

      }
    }).mount('#app')
  </script>

这里我们通过CDN来直接使用Vue,然后我们使用v-for去遍历一个数组,生成一段li列表。当我们在Vue里面的template模板里面写入一串代码时,其实对Vue的编译器来说都是输入了一段字符串。Vue最后会把这些字符串变成真实的html结构。

首先,Vue会通过编译将模板转换成渲染函数(render), 然后执行这个渲染函数就会得到一个虚拟DOM, 我给大家看一张图片:(图片来源于网络)

vue中别再使用index做key啦!

我简单的用JS代码来描述一下这个虚拟DOM对象,用来帮助大家更好的去理解:

<script>
    // compiler 编译器
    let VDom = { 
      tagName: 'ul',
      props: {
        class: 'list',
        id: 'list'
      },
      children: [
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['html']
        },
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['css']
        },
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['vue']
        },
      ]
    }
  </script>

大家可以看到,其实虚拟DOM本质上就是一个对象,只是它是抽象的,是轻量级的,并且它与实际的DOM结构相对应,最终通过一系列的比较之后它也会生成真实的html结构(后面会讲)。

假设我们将响应式数据源list进行更改,将vue替换为js,那么就会生成一个新的虚拟DOM,并且最后一个children的内容为vue

let newDom = {  // 新虚拟dom
      tagName: 'ul',
      props: {
        class: 'list',
        id: 'list'
      },
      children: [
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['html']
        },
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['css']
        },
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['js']
        },
      ]
    }

因为对于Vue来说,当数据源变更了,编译器是一定会进行编译的,那么也就导致生成了一份新的虚拟DOM,这时候就产生了两份虚拟DOM,一份新的,一份旧的。因为虚拟DOM最终是要转化为真实DOM结构而成为html,如果直接废除旧的虚拟DOM,而将新的虚拟DOM重新渲染到浏览器是十分不友好的,它会造成大量的回流重绘,这是十分消耗浏览器的性能的。

所以Vue官方就想出了一个新的办法,当数据源更新后,我们手上存在两份虚拟DOM,一份是旧的,一份是新的,而且在常见情况下,比如上述代码,我们只是将ul中的一个li的内容改了一下,而浏览器只需要重新渲染这个li就行了,根本没有必要向刚刚一样去渲染整个页面。

因此, diff算法就出现了。

diff算法

浏览器去重新渲染整个页面造成的开销是十分大的,比如在上面的情况中,我们只需要通过某种算法去找到两份虚拟DOM结构的不同,然后这个算法会生成一个补丁包patch,然后通过patch去更改真实的DOM结构。

这就是diff算法,通过比对新老虚拟DOM的不同,然后生成一个补丁包patch,变成真实DOM。

diff算法的代码是十分复杂的,而我们并不用知道diff算法的代码,我们只需要知道diff算法的实现原理,这同样也是我们在面试过程中面试官喜欢问到的。

  • 过程
  1. 同层比较,是不是相同的结点,不相同直接废弃老DOM
  2. 是相同节点,比较节点上的属性,产生一个补丁包
  3. 继续比较下一层的子节点,采用双端队列的方式,尽量复用,产生一个补丁包
  4. 同上

vue中别再使用index做key啦!

对端对列就是头头比较,尾尾比较,头尾比较。

并且,为了减少开销,diff算法是会尽量复用相同的结点的。而key的存在且唯一就是保证了diff算法可以更好地去复用结点。

Vue为什么不建议使用index作为key

在Vue中,对于diff算法,只要结点是相同的,那么它就是可以复用的,还是用上述的例子,我用key作为index:

!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <ul class="list" id="list">
      <li class="item" v-for="(item, index) in list" :key="index">{{item}}</li>
    </ul>

    <button @click="add">add</button>
  </div>

  <script>
    const { createApp, ref } = Vue

    createApp({
      setup() {

        const list = ref(['html', 'css', 'vue'])

        const add = () => {
          list.value.unshift('js')
        }

        return {
          add
        }

      }
    }).mount('#app')
  </script>

我们写了一个add函数,它的作用是在list数组前面添加一个js,并且将它绑定为按钮的点击事件。如果这里我们使用index作为key,那么我们先来写一下虚拟DOM结构:

let OldDom = [
    {
        tagName: 'li',
        value: 'html',
        key: 0
    },
    {
        tagName: 'li',
        value: 'css',
        key: 1
    },
    {
        tagName: 'li',
        value: 'vue',
        key: 2
    },
]

let NewDom = [
    {
        tagName: 'li',
        value: 'js',
        key: 0
    },
    {
        tagName: 'li',
        value: 'html',
        key: 1
    },
    {
        tagName: 'li',
        value: 'css',
        key: 2
    },
    {
        tagName: 'li',
        value: 'vue',
        key: 3
    }
]

在使用index作为下标的基础上,OldDOM为旧的虚拟DOM,而NewDOM则是在我们点击按钮后,触发点击事件,将js推入数组前面,生成的一个新的虚拟DOM。我们可以看到,OldDOM的1, 2, 3结点跟NewDOM的2, 3, 4结点其实是相同的,diff算法应该将他们复用,但是因为它们的key不同,那么双端队列进行比较的时候就会认为结点之间是不相同的,所以不会复用,这样会浪费性能。

或是说我将list进行翻转一下:

let OldDom = [
    {
        tagName: 'li',
        key: 0,
        value: 'html'
    },
    {
        tagName: 'li',
        key: 1,
        value: 'css'
    },
    {
        tagName: 'li',
        key: 2,
        value: 'js'
    },
]

let NewDom = [
    {
        tagName: 'li',
        key: 0,
        value: 'js'
    },
    {
        tagName: 'li',
        key: 1,
        value: 'css'
    },
    {
        tagName: 'li',
        key: 2,
        value: 'html'
    },
]

我们可以发现,新旧虚拟DOM只是调转了一下顺序,是可以进行复用的,但是因为用了数组下标作为key值,导致数组翻转后下标也随之变动了,翻转之后key值并不相同,所以diff算法不能很好的复用。

所以,我们需要人为的去提供key值,而不能使用数组的index去作为下标。

假设我们通过某种手段,去给他们添加key值:

let OldDom = [
    {
        tagName: 'li',
        key: 1,
        value: 'html'
    },
    {
        tagName: 'li',
        key: 2,
        value: 'css'
    },
    {
        tagName: 'li',
        key: 3,
        value: 'js'
    },
]

let NewDom = [
    {
        tagName: 'li',
        key: 3,
        value: 'js'
    },
    {
        tagName: 'li',
        key: 2,
        value: 'css'
    },
    {
        tagName: 'li',
        key:1,
        value: 'html'
    },
]

在diff算法进行比对的时候,发现结点是完全相同的,那么可以复用结点。

如果不使用key值

那么可能小伙伴们就有疑问了,那如果我在使用v-for时,不用key值,那么元素翻转完它们的结点还是相同的。

首先在Vue中,如果我们在v-for循环时不使用key,那么首先程序会发出警告,其次,使用 key 可以帮助 Vue 准确地复用和更新 DOM 元素。

我们再想一个情景,假如我们现在list = [html, css, css], 我们将list进行一个翻转:

let OldDom = [
    {
        tagName: 'li',
        value: 'html'
    },
    {
        tagName: 'li',
        value: 'css'
    },
    {
        tagName: 'li',
        value: 'css'
    },
]

let NewDom = [
    {
        tagName: 'li',
        value: 'css'
    },
    {
        tagName: 'li',
        value: 'css'
    },
    {
        tagName: 'li',
        value: 'html'
    },
]

那么我们再比对新旧虚拟DOM时,发现NewDom中第一个结点跟OldDom中第二个和第三个结点都对应,那么根据diff算法尽量复用结点的规则,那么应该去保留哪一个呢?是第二个还是第三个。

总结

相信到现在你也明白了在v-for循环中key的意义了,它就是为了去准确地复用虚拟DOM结点

并且如果在日后,我们的一些操作涉及到对列表的一些增删改查,那么我们千万不能使用数组的index去作为key值,这样的话diff算法就不能很好的为我们去节省性能 。