1
我第一次接触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的执行),才能写出一个受欢迎的组件。


蓝德锦
54 声望3 粉丝