vue

(Vue) key值的作用

2020-03-28  本文已影响0人  陈成熟

今天思考一个问题,在子组件中,key值的作用是什么?
如果一个组件,<A key="1" />改边key的值,<A key="2" />,发生什么?
实践出真理,测试一下:
首先,创建一个子组件:

//components/keyCom.vue
<template>
  <div>
      <p>{{ptext}}</p>
  </div>
</template>

一个非常简单的组件,在各个生命周期上,绑定事件:

export default {
    data(){
        return {
            ptext:"测试文本"
        }
    },
    beforeCreate(){
        console.log('enter beforeCreate')
    },
    created(){
        console.log('enter created')
    },
    beforeMount(){
        console.log('enter beforeMount')
    },
    mounted(){
        console.log('enter mounted')
    },
    beforeDestroy(){
        console.log('enter beforeDestroy')
    },
    destroyed(){
        console.log('enter destroyed')
    }
}

接下来,通过父组件修改子组件的key值:

<template>
<div>
  <Key-Com :key="key" />
  <a class='change-btn' @click='changeKey()'>切换key</a>
</div>
</template>

<script>
import KeyCom from '@/components/keyCom'
export default {
    data(){
        return {
            key:1
        }
    },
    components:{
        KeyCom
    },
    methods:{
        changeKey:function(){
            this.key=2;
        }
    }
}
</script>

看看运行起来是什么情况:

切换key值之前

点击按钮之后,发现:

修改了key值之后
组件经历了一个全新的生命周期,这是为何?为什么同样一个组件,仅仅改变了它上面的key值,就会重新挂载一个新组件?
之前了解到,key值的最大作用,是在渲染列表的时候,diff算法使用到,那么我们就来看看diff的过程是如何?
分析vue的源码,可以知道,diff算法是从patch函数开始:
patch:function(oldVnode,vnode){
    if(sameVnode(oldVnode,vnode)){
       patchVnode(oldVnode,vnode)
    } else {
       const oEl = oldVnode.el;
       let parentEle = api.parentNode(oEl);
       createEle(vnode);
       if(parent!==null){
          api.insertBefore(parentEle,vnode.el,api.nextSibling(oEl));
          api.removeChild(parentEle,oldVnode.el);
          oldVnode = null
       }
    }
    return vnode;
},

通过patch函数,可以看到,首先需要对比两个节点是否是相同节点,(相同的组件,难道不是相同节点吗?)
进入sameVnode函数看看:

        sameVnode(a,b){
            return (
                a.key === b.key && // key值
                a.tag === b.tag && // 标签名
                a.isComment === b.isComment && //是否为注释节点
                //是否都定义了data,data包含一些具体信息,例如onclick
                isDef(a.data) === isDef(b,data) &&
                sameInputType(a,b) //当标签是input,type必须相同
            )
        }

恍然大悟,原来在diff的时候,不仅是对比元素的标签名,还会去对比元素的key值,key值一旦改变,就算子节点的内容一模一样,也是会进入到patch函数的else中,那么这个时候,执行的操作就是新建新组件=>删除旧组件=>添加新组件。
因此,可以看到生命周期是新组件的生命周期先执行,再进行旧组件的销毁,接着挂载新组件。

emmmm...
那么再思考深一层的问题,如果是列表渲染的时候,key值设为id,和index会有什么区别呢??
同样的做一个测验:创建一个子组件,子组件里包含一个孙子组件

<template>
  <div>
    {{text}}
    <input v-model="x">
    <button @click="onDelete">delete</button>
  </div>
</template>

<script>
export default {
  name: "Child",
  props: ["text"],
  data() {
    return {
      x: "在这输入"
    };
  },
  methods: {
    onDelete() {
      this.$emit("delete");
    }
  }
};
</script>

接着,在原先的<key-com/>组件里:

<template>
  <div>
      <Child class="child" v-for="(item,i) in array" :key='i' :text="item" @delete="remove(i)" />
      <Child class="child" v-for="(item,i) in array2" :key='item.id' :text="item.value" @delete="remove2(i)" />
  </div>
</template>

创建2个Child组件,它们的区别就是一个使用index作为key,一个使用id作为key:

data(){
    return {
            ptext:"测试文本",
            array: ['111','222','333'],
            array2: [{id:1,value:'文本1'},{id:2,value:'文本2'},{id:3,value:'文本3'}]
    }
}

运行之后就可以看到它们的区别:

image.png
先看上面的三行,这个是使用index作为key值的组件,当修改其中222这行的input值,然后点击删除:
改变第二行的input值
删除第二项之后
删除之后发现,这与我们的预知不符呀,因为 data 里的数组从 [1,2,3] 变成了 [1,3]。
这个可以看到vue中数组遍历的规则:首先对比1和1,发现1没变,然后对比2和3,发现2变成了3,接着对比3和undefined,把3删掉。
所以步骤是:2变成3=>删除3。
那么在删除的时候,因为input的值是孙子组件,里面的值不受2变成3的影响,所以就地复用
再看下面这个列表,使用id作为key值。当我们修改了第二项的input值,然后删除第二项的时候,会把第二项完全删掉,符合我们的预期:
修改第二项的input值
删除第二项之后
原本的数组是:
array2: [
  {id:1,value:'文本1'},
  {id:2,value:'文本2'},
  {id:3,value:'文本3'}
]

点击删除之后数组是:

array2: [
  {id:1,value:'文本1'},
  {id:3,value:'文本3'}
]

先对比id从[1,2,3]变成了[1,3],即第二项被删除了。
因此:key值为何不能用index作为值?
如果你用index作为key值的时候,在删除第二项时,index就从1,2,3变成1,2;而不是1,3。

结论

VUE是通过比对组件自身新旧vdom进行更新的。key的作用是辅助判断新旧vdom节点在逻辑上是不是同一个对象。
因此可以确定,渲染列表时,key值需要一个唯一确定的id来赋值。

上一篇 下一篇

猜你喜欢

热点阅读