vue中别再使用index做key啦!

简介: vue中别再使用index做key啦!

前言

既然我们这里提到了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, 我给大家看一张图片:(图片来源于网络)

image.png

我简单的用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. 同上

image.png

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

并且,为了减少开销,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算法就不能很好的为我们去节省性能 。

相关文章
|
19天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
34 1
vue学习第四章
|
19天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
28 1
vue学习第九章(v-model)
|
19天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
33 1
vue学习第十章(组件开发)
|
25天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
25天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
25天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
25天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
24天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
36 3
|
26天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
24天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
47 2