深入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会采用就地复用的原则,也就是按顺序来比较节点,比如:

当你插入一条数据时

    
    旧节点 新节点
  • 1
  • 1
  • 2
  • 5
  • 3
  • 2
  • 4
  • 3
  • 4

新节点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'
    }
]
  • {{item.title}}

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

    
    旧节点 新节点
  • 新闻标题4
  • 新闻标题1
  • 新闻标题1
  • 新闻标题2
  • 新闻标题2
  • 新闻标题3
  • 新闻标题3

vue根据key去匹配节点,新闻的id是不变的,所以找到旧的对应节点下的内容也是一致,此时,只需要生成

  • 新闻标题4
  • ,并在对应的位置插入即可,这样的dom更新效率更高

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

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

            
      旧节点 新节点
    • 新闻标题1
    • 新闻标题4
    • 新闻标题2
    • 新闻标题1
    • 新闻标题3
    • 新闻标题2
    • 新闻标题3

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

    key错误的经典案例

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

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

    let list = [
        {
            name: '姓名'
        },
        {
            name: '年龄'
        }
    ]
    {{item.name}}:

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

    这是因为的值在输入框中是临时DOM状态,这个状态跟随的是key,所以才会出现这种情况,但是实际写代码很少会出现这种情况,因为写一般是为了获取输入,肯定会通过v-model来绑定值的,一旦绑定了值,中的值就不是临时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,就可以轻易触发组件的重新创建

        
        
        export default {
            data() {
                return {
                    width:  100,
                    key: 1
                }
            },
            methods: {
                changeWidth() {
                    this.width += 100
                    this.key++
                }
            }
        }

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

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

    你可能感兴趣的:(深入vue中key的作用:diff算法下dom节点的高效复用)