Vue 列表渲染时:key的作用,以及它存在的问题,和解决方案

1,143 阅读4分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。

<span v-for="(time,index) in arr" :key="index">{{time}}<span>
嗨,大家好,我是Starqin,今天要讲述的就是上面代码中:key的作用,以及它的一些问题

假设我有以下数据

    const vm = new Vue({
            el: '.box',
            data: {
                userinof: [{
                    name: 'Starqin',
                    age: 22,
                    pro: '程序员',
                    hobby: '看科幻电影、电子产品'
                }, {
                    name: 'W-js',
                    age: 23,
                    pro: '平面设计师',
                    hobby: '打篮球、汽车迷'
                }]
            }
        })

接下来我将使用v-for将上面的数据渲染出来

图片.png 渲染结果

图片.png 思考一下,上面代码图中黄色框中的key有没有被浏览器解析到DOM中呢?

答案是没有的

key在哪?有什么用?

图片.png 这是为什么呢?key去哪里呢?

其实key是存在的,只不过,这里的key是vue底层在使用,放在了vue虚拟DOM上,请看下图的对比

图片.png

下面来说一下为什么vue的虚拟DOM上要有这个key

我们都知道,vue在将虚拟dom转为真实dom之前,都会做一遍Diff算法,将两个DOM做一个比较,渲染存在差异的部分,留下相同的部分,需要十分注意的是,这里的比较是对节点进行比较,而不是简单的对元素进行比较。

vue是以元素上的key值进行比较的,例如下图

图片.png

当vue在进行比较的时候发现,原来的虚拟DOM上并没有key=2的存在,于是就会直接将姓名为张三的DOM渲染成真实DOM

也就是说,这里的key是支撑DIFF算法的关键。既然如此关键,那么这里的key我们可以不写吗?

答案是可以的,如果我们不写key,那么vue会默认将v-for的索引值作为key的值,不管我们写不写,其虚拟DOM上肯定有key这个属性存在。

key为索引时会出现什么BUG

既然Vue会默认生成key,那么为什么总有人要劝我多此一举,写上key呢?

带着疑问,我们来看一下,下面这个例子
现在我需要动态的将对象zangsan,插入到上面例子的前面(在Starqin的前面插入一个成员),并渲染到页面

<body>
    <div class="box">
        <button @click="addUser">点击我添加张三</button>
        <div class="userbox" v-for="(v,i) in userinof" :key="i">
            <p>姓名:{{v.name}}</p>
            <p>爱好:{{v.hobby}}</p>
            <p>年龄:{{v.age}}</p>
            <p>职业:{{v.pro}}</p>
            <input type="text">
        </div>
    </div>

    <script>
        const vm = new Vue({
            el: '.box',
            data: {
                userinof: [{
                    name: 'Starqin',
                    age: 22,
                    pro: '程序员',
                    hobby: '看科幻电影、电子产品'
                }, {
                    name: 'W-js',
                    age: 23,
                    pro: '平面设计师',
                    hobby: '打篮球、汽车迷'
                }],
                //这不是userinof数组中的元素,而是data下的对象
                zansan: {
                    name: '张三',
                    age: 55,
                    pro: '家里蹲大学特聘吹牛专家教授',
                    hobby: '吹牛'
                }
            },
            methods: {
                addUser() {
                   this.userinof.unshift(this.zansan)
                }
            },
        })
    </script>
</body>

请看渲染页面图

图片.png 注意,此时我新增了一个按钮,以及在模板中新增了一个input框,当我点击这个按钮的时候,会将上面数据中的zansan对象追加到userinof数组的开头。还请大家注意在点击这个按钮之前两个输入框中的值,Starqin下的输入框中是Starqin,W-js下的input框中是W-js

当我点击按钮之后,程序运行的效果如下图

图片.png 心细的您是不是发现,我输入框的文字发生了错位。这是为什么呢?难道这是vue渲染页面的BUG吗?其实不是,请让我们将视线重新聚焦到key身上,请看虚拟DOM对比图

图片.png 形成上面BUG的原因是,由于我们是在原来数组的前面插入了一个zangsan对象,这也就导致原来索引是0的Starqin,索引是1的W-js,现在变成了Starqin索引为1,W-sj索引为2,索引变化请看下图

图片.png 再加上我们是以数组索引作为key的值的,最终导致新旧虚拟DOM依赖key值在做对比的时候,发现虚拟DOM对比图上红色框内的节点与旧虚拟DOM不一样,于是进行重新渲染,而绿色框中的节点一样,直接复用,最终形成了上面input框发生错位的现象

解决Key为索引产生的BUG

怎么解决这个BUG呢?

其实解决这个问题很简单,我们只要将数据中的唯一的值赋值给key就可以完美解决这样的BUG,例如我将用户姓名作为唯一值赋值给key
修改后的部分代码

<div class="box">
        <button @click="addUser">点击我添加张三</button>
        <div class="userbox" v-for="(v,i) in userinof" :key="v.name">
            <p>姓名:{{v.name}}</p>
            <p>爱好:{{v.hobby}}</p>
            <p>年龄:{{v.age}}</p>
            <p>职业:{{v.pro}}</p>
            <input type="text">
        </div>
    </div>

修改后点击按钮 图片.png 问题完美解决\

这又是为什么呢?请看下图

图片.png 此时新旧虚拟DOM绑定的key值都不会发生改变,形成了一一对应的关系,在作比较时,发现旧虚拟DOM上没有key值为张三的,于是直接在旧真实DOM上渲染了一个新元素。

总结:

  1. 如果数据具有唯一标识时,key值必须是那个唯一标识,没有时,随便。
  2. key的存在是为了更好的为Vue的diff算法提供支撑