vue中别再使用index做key啦!

前言

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

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

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

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

虚拟DOM

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

html 复制代码
<!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, 我给大家看一张图片:(图片来源于网络)

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

js 复制代码
<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

js 复制代码
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. 同上

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

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

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

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

html 复制代码
!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结构:

js 复制代码
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进行翻转一下:

js 复制代码
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值:

js 复制代码
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进行一个翻转:

js 复制代码
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算法就不能很好的为我们去节省性能 。

相关推荐
uhakadotcom几秒前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰7 分钟前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪15 分钟前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪24 分钟前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy1 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom2 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom2 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom2 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom2 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试