深入vue中key的作用:diff算法下dom节点的高效复用

我第一次接触key,是在学习v-for循环时,不加key标识会报错,对于初学者来说,听过key的原理也会懵懵懂懂,一知半解。
当我用了很久的vue,在回头看key的原理时,才彻底弄懂了key的原理和作用。

虚拟Dom渲染

要想了解key的原理,绕不开的就是虚拟dom的渲染过程,因为key最大的作用就是标识节点,以便相同的节点可以被高效复用。

在vue渲染过程中,vue会先生成虚拟dom,也就是用JavaScript中的对象完整描述真实的dom节点,生成的虚拟dom类似如下:

[
    {
        tagName: 'div',
        children: [
            {
                tagName: 'p',
                props: {
                    class: 'row'
                }
                // ...
            }
        ],
        props: {
            id: 'app'
        }
        // ...
    }
]

生成虚拟dom后,再通过虚拟dom渲染出真实节点,页面便有了,此时页面存在真实dom节点和虚拟dom节点两部分。

当你修改了dom节点后,虚拟dom会重新生成,此时页面有新旧两个虚拟dom树,vue会比较两个虚拟dom对象,把完全相同的虚拟dom对应的真实dom节点直接复用,不同的部分进行真实dom更新,这个比较的过程就是所谓的diff算法,这样处理使得真实的dom的更新最小化,从而使得性能更优。

key的用处

在比较虚拟dom的过程中,如果没有key,vue会采用就地复用的原则,也就是按顺序来比较节点,比如:

当你插入一条数据时

    <ul>
        旧节点          新节点
        <li>1</li>      <li>1</li>
        <li>2</li>      <li>5</li> 
        <li>3</li>      <li>2</li> 
        <li>4</li>      <li>3</li> 
                        <li>4</li> 
    </ul>

新节点li-1和旧节点li-1比较,完全一致,不生成新dom节点,直接复用旧dom节点;

新节点li-5和旧节点li-2比较,li一样,内容不一样,复用li节点,文本节点5重新生成并替换旧的文本节点2

...

新节点li-4找不到旧的节点对比,直接创建新的dom节点

以此类推,如果li里不是简单的数字,而是其他复杂的组件,那么操作dom的成本就更高了。

如何让vue在diff时知道新节点和哪些旧节点去比较呢?那就是key起的作用,如果给节点绑定了key属性,那vue在diff的时候,会找到与新节点的key相同的旧节点进行比较,比如我们渲染了一页新闻列表:

let list = [
    {
        id: 1,
        title: '新闻标题1'
    },
    {
        id: 2,
        title: '新闻标题2'
    },
    {
        id: 3,
        title: '新闻标题3'
    }
]
<ul>
    <li v-for="item in list" :key="item.id">
        {{item.title}}
    </li>
</ul>

当你插入一条数据在第一条,得到新旧节点如下:

    <ul>
        旧节点                          新节点
                                        <li key="4">新闻标题4</li>
        <li key="1">新闻标题1</li>      <li key="1">新闻标题1</li> 
        <li key="2">新闻标题2</li>      <li key="2">新闻标题2</li> 
        <li key="3">新闻标题3</li>      <li key="3">新闻标题3</li> 
    </ul>

vue根据key去匹配节点,新闻的id是不变的,所以找到旧的对应节点下的内容也是一致,此时,只需要生成 <li>新闻标题4</li> ,并在对应的位置插入即可,这样的dom更新效率更高

在for循环中,可以使用index作为key,但是这会导致数据在插入、删除等破坏列表顺序的操作时,造成效率底下,使用index当key和不加key的效果是一直的,因为当插入一条数据时,节点的key也会跟着改变,还是用新闻列表举例:

当使用index作为key时,新旧节点key的变化和对比关系:

        <ul>
        旧节点                          新节点
        <li key="0">新闻标题1</li>      <li key="0">新闻标题4</li>
        <li key="1">新闻标题2</li>      <li key="1">新闻标题1</li> 
        <li key="2">新闻标题3</li>      <li key="2">新闻标题2</li> 
                                        <li key="3">新闻标题3</li> 
    </ul>

你会发现,key相等的节点下的内容几乎都不一致,这就造成了大规模节点的丢弃和重新渲染。

key错误的经典案例

在网上很多讲解key作用的案例中,都会用到<input />作用案例,大概逻辑如下:

v-for渲染一个列表,使用index作为key,每个列表下都会有一个<input/>

let list = [
    {
        name: '姓名'
    },
    {
        name: '年龄'
    }
]
<div v-for="(item,index) in list" :key="index">
   {{item.name}}: <input />
</div>

当列表被插入一个新值时,input并不会跟随原来的item,比如在姓名后面的<input />输入了内容,当list在第一位插入一个班级时,原本在姓名<input />的内容变成了班级<input />的内容了

这是因为<input />的值在输入框中是临时DOM状态,这个状态跟随的是key,所以才会出现这种情况,但是实际写代码很少会出现这种情况,因为写<input />一般是为了获取输入,肯定会通过v-model来绑定值的,一旦绑定了值,<input />中的值就不是临时DOM状态,那就不会出现上面那种情况了。

key的其他用途 - 更新key实现组件重新渲染

key有个不太正规,但某些情况非常好用的功能,比如有些组件,在创建的时候有初始化的行为:

export default {
    // ...
    data() {
        return {
            _width: 0
        }
    },
    props: {
        width: {
            type: [Number,String]
        }
    },
    created() {
        if (typeof this.width === 'number') {
            this._widht = this.width + 'px'
        } else {
            this._widht = this.width
        }
    }
}

上述的代码是一个简单的列子,宽度可以传数字或字符串类型,如果传的是数字类型,创建组件的时候就拼接上px(该例子可以使用计算属性完成,此处为了展示)。

类似的组件,在props修改的时候,_width并不会被更新,除非监听width属性。

如果组件的生命周期里还写了很多依赖props的逻辑代码,那不如让组件重新创建好了。

利用key,就可以轻易触发组件的重新创建

    <my-component :key="key" :width="width"></my-component>
    <button @click="changeWidth">修改组件宽度</button>
    export default {
        data() {
            return {
                width:  100,
                key: 1
            }
        },
        methods: {
            changeWidth() {
                this.width += 100
                this.key++
            }
        }
    }

这样,在每次修改width时,组件都会重新被创建。

虽然达到了重新执行生命周期的目的,但牺牲了重新创建组件的性能,所以我们在设计组件的时候,应多考虑props被改动的情况(比如用计算属性代替created的执行),才能写出一个受欢迎的组件。

45 声望
3 粉丝
0 条评论
推荐阅读
一次vue-cli4大项目升级到vite的经历
使用vue-cli4的项目,业务写多了之后开发运行和打包都慢了很多,为了提升开发体验以及更新团队技术框架,需要升级到更高级的脚手架上,两种方案:一是升级到vue-cli5,二是升级到最新的vite。

蓝德锦3阅读 6.4k评论 5

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木172阅读 13.8k评论 10

手把手教你写一份优质的前端技术简历
不知不觉一年一度的秋招又来了,你收获了哪些大厂的面试邀约,又拿了多少offer呢?你身边是不是有挺多人技术比你差,但是却拿到了很多大厂的offer呢?其实,要想面试拿offer,首先要过得了简历那一关。如果一份简...

tonychen152阅读 17.7k评论 5

封面图
正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 8.4k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy48阅读 6.9k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 6.8k评论 12

封面图
45 声望
3 粉丝
宣传栏