面试题:vue中属性key的作用(了解diff),为什么不推荐用 index作为key

1. 官方文档有关key的说明

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。 而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。 有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

2. 举个例子

可以运行一下代码,改变key为 索引idx(或者不设置key) 或者 item.id, 看下效果

  • 当索引idx,作为key的时候(或者不设置key),选中一项,再点击添加新值的时候,选中的项会改变
  • 当item.id作为key的时候,选中一项,再添加的时候,选中项不会变化
js 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

</head>

<body>
    <div id="app">
        <div>
            <input type="text" v-model="name">
            <button @click="add">添加</button>
        </div>
        <ul>
            <!-- <li v-for="(item,idx) in list" :key="idx"> -->
                <li v-for="item in list" :key="item.id">
                <input type="checkbox">ID:{{item.id}} -------name:{{item.name}}
            </li>
        </ul>
    </div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                name: '',
                list: [{
                        id: 0,
                        name: '数据1'
                    },
                    {
                        id: 1,
                        name: '数据2'
                    },
                    {
                        id: 2,
                        name: '数据3'
                    }
                ]
            },
            methods: {
                add() {
                    const newUser = {
                        id: this.list.length,
                        name: this.name
                    };
                    this.list.unshift(newUser);
                }
            }
        })
    </script>
</body>
</html>

可以看出,当key值为item.id的时候,checkbox选中项在添加新项的时候不会变化。为什么会这样呢?我们往下看

3. diff

在vue中,当数据发生变化,会触发更新,更新过程的核心就是新旧 vnode的diff。

diff的原理就是对前后的节点树同一层的节点进行对比,一层一层对比,如下

当某一层有很多相同的节点时,也就是列表节点时,diff算法的更新过程默认情况下也是遵循以上原则。

如下

我们希望可以在B和C之间加一个F,diff算法默认执行起来是这样的

即把C更新成F,D更新成C,E更新成D,最后再插入E,相当没有效率!

所以我们需要使用key来给每个节点做一个唯一标识,diff算法就可以正确的识别此节点,找到正确的位置去插入新的节点。如下图

在vue中,判断新旧节点的sameVnode(oldVnode, vnode)函数如下

js 复制代码
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)))
  )
}

可以看到,判断两个节点是否一样,首先判断了节点的key

因此,唯一的key值,会在diff比较新旧节点是否相同时,起到关键作用,起到更高效的更新虚拟节点的作用

4. 为什么不推荐索引index作为key

可以参考例子中的代码,在增删变化的场景中,元素的index可能是会变化的,diff算法时比较同级之间的不同,以key来进行关联,当对数组进行下标的变换时,比如删除第一条数据,那么以后所有的index都会发生改变,那么key自然也跟着全部发生改变,所以index作为key值是不稳定的,而这种不稳定性有可能导致性能的浪费,导致diff无法关联起上一次一样的数据。因此,能不使用index作为key就不使用index

5. 为什么不要用随机数作为key?

js 复制代码
<item
  :key="Math.random()"
  v-for="(num, index) in nums"
  :num="num"
  :class="`item${num}`"
/>

其实我听过一种说法,既然官方要求一个 唯一的key,是不是可以用 Math.random() 作为 key 来偷懒?这是一个很鸡贼的想法,看看会发生什么吧。

首先 oldVnode 是这样的:

js 复制代码
[
  {
    tag: "item",
    key: 0.6330715699108844,
    props: {
      num: 1
    }
  },
  {
    tag: "item",
    key: 0.25104533240710514,
    props: {
      num: 2
    }
  },
  {
    tag: "item",
    key: 0.4114769152411637,
    props: {
      num: 3
    }
  }
];

更新以后是:

js 复制代码
[
  {
    tag: "item",
+   key: 0.11046018699748683,
    props: {
+     num: 3
    }
  },
  {
    tag: "item",
+   key: 0.8549799545696619,
    props: {
+     num: 2
    }
  },
  {
    tag: "item",
+   key: 0.18674467938937478,
    props: {
+     num: 1
    }
  }
];
 

可以看到,key 变成了完全全新的 3 个随机数。

上面说到,diff 子节点的首尾对比如果都没有命中,就会进入 key 的详细对比过程,简单来说,就是利用旧节点的 key -> index 的关系建立一个 map 映射表,然后用新节点的 key 去匹配,如果没找到的话,就会调用 createElm 方法 重新建立 一个新节点。

js 复制代码
// 建立旧节点的 key -> index 映射表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
 
// 去映射表里找可以复用的 index
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 一定是找不到的,因为新节点的 key 是随机生成的。
if (isUndef(idxInOld)) {
  // 完全通过 vnode 新建一个真实的子节点
  createElm();
}

也就是说,咱们的这个更新过程可以这样描述:123 -> 前面重新创建三个子组件 -> 321123 -> 删除、销毁后面三个子组件 -> 321

这样一来创建新的组件和销毁组件的成本就非常的大,本来仅仅是对组件移动位置就可以完成的更新,被我们毁成这样了。

总结

  1. 用组件唯一的 id(一般由后端返回)作为它的 key,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个 key,并保证这个 key 在组件整个生命周期中都保持稳定。
  2. 别用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
  3. 千万别用随机数作为 key,不然旧节点会被全部删掉,新节点重新创建。
相关推荐
mubeibeinv几秒前
项目搭建+图片(添加+图片)
java·服务器·前端
逆旅行天涯7 分钟前
【Threejs】从零开始(六)--GUI调试开发3D效果
前端·javascript·3d
小蜗牛慢慢爬行17 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
m0_7482552628 分钟前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
web147862107231 小时前
C# .Net Web 路由相关配置
前端·c#·.net
m0_748247801 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖1 小时前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案11 小时前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
ThisIsClark1 小时前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
m0_748254881 小时前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl