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

相关文章
|
7月前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
603 2
|
6月前
|
缓存 JavaScript
vue中的keep-alive问题(2)
vue中的keep-alive问题(2)
526 137
|
10月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
1032 0
|
10月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
9月前
|
人工智能 JSON JavaScript
VTJ.PRO 首发 MasterGo 设计智能识别引擎,秒级生成 Vue 代码
VTJ.PRO发布「AI MasterGo设计稿识别引擎」,成为全球首个支持解析MasterGo原生JSON文件并自动生成Vue组件的AI工具。通过双引擎架构,实现设计到代码全流程自动化,效率提升300%,助力企业降本增效,引领“设计即生产”新时代。
632 1
|
10月前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
642 17
|
9月前
|
JavaScript 安全
在 Vue 中,如何在回调函数中正确使用 this?
在 Vue 中,如何在回调函数中正确使用 this?
437 0
|
10月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
603 1
|
10月前
|
存储 JavaScript 前端开发
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
905 0
|
10月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
467 0

热门文章

最新文章