vue为什么要加key?

vue源码学习


一、不加key或者key是索引会怎么样?

 <div id="app">
   <div>
        <input type="text" v-model="myName">
        <button @click="add">增加</button>
    </div>
    <ul a='1' style="color:red">
        <li v-for="item in arr"> <input type="checkbox">{{item.name}}</li>
    </ul>
</div>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
<script>
    const vm = new Vue({
        el:'#app',
        data: {
            arr:[
                {id:'1', name: '西瓜'},
                {id:'2', name: '葡萄'},
                {id:'3', name: '冰镇汽水'}
            ],
            myName: '',
            myId: 3,
        },
        methods: {
            add() {
                this.arr.unshift({id:++this.myId, name: this.myName});
                this.myName= '';
            }
        }
       
    })
    vm.$mount('#app')
</script>  

运行之后长这样:
在这里插入图片描述
当不加key时,增加了一个柠檬选项,发现勾选的数据变了:
在这里插入图片描述
在这里插入图片描述
不加key会出现bug,那加key,把key变成索引会怎么样?
在这里插入图片描述
结果是一样的,除非把key换成唯一标识。

二、vue为什么要加key?

由上文可知,不写key或把key写成索引都会有问题的。为什么呢?

渲染真实的dom时,并不是暴力覆盖原有的dom,而是比对新旧两个vnode(虚拟节点),如果不是同一个节点,删除老的,替换成新的;如果是同一个节点,就复用老节点,增加新节点的属性。

对应源码:\src\core\vdom\patch.js

判断两个节点是否相同:

export function isSameVnode(vnode1, vnode2) {
    return vnode1.tag === vnode2.tag && vnode1.key === vnode2.key;
}

标签名和key相同会被判断为同一个节点。
Diff算法中,比较相同节点有一些优化:头头比较(新旧节点的起始索引)、尾尾比较、头尾比较、尾头比较:

function updateChildren(el, oldChildren, newChildren) {
    // 操作列表
    // vue2采用双指针的方法,比较两个节点
    let oldStartIndex = 0;
    let newStartIndex = 0;
    let oldEndIndex = oldChildren.length - 1;
    let newEndIndex = newChildren.length - 1;

    let oldStartVnode = oldChildren[0];
    let newStartVnode = newChildren[0];

    let oldEndVnode = oldChildren[oldEndIndex];
    let newEndVnode = newChildren[newEndIndex];

    while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        // 双方有一方头指针 > 尾指针则停止循环
        if (isSameVnode(oldStartVnode, newStartVnode)) { // 头头
            patchVnode(oldStartVnode, newStartVnode) // 如果相同节点,则递归比较子节点
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
            // 比较开头节点
        } else if (isSameVnode(oldEndVnode, newEndVnode)) { // 尾尾
            patchVnode(oldStartVnode, newStartVnode) // 如果相同节点,则递归比较子节点
            oldEndVnode = oldChildren[--oldEndVnode];
            newEndVnode = newChildren[--newEndVnode];
            // 比较开头节点
        } else if (isSameVnode(oldEndVnode, newStartVnode)) {
            // 交叉比对 abdc dabc
            if(isSameVnode(oldEndVnode, newStartVnode)) { // 尾头
                patchVnode(oldEndVnode, newStartVnode);
                el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 将老的尾巴移动到新的前面
                oldEndVnode = oldChildren[--oldEndIndex];
                newStartVnode = newChildren[++newStartIndex]
            }
        } else if (isSameVnode(oldStartVnode, newEndVnode)) { // 头尾
            // 交叉比对 abdc dabc
            if(isSameVnode(oldEndVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newEndVnode);
                el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 将老的尾巴移动到新的前面
                oldStartVnode = oldChildren[++oldEndIndex];
                newEndVnode = newChildren[--newStartIndex];
            }
        }
    }
    if (newStartIndex <= newEndIndex) { // 多余的插入
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            let childEl = createElm(newChildren[i]);
            
            // 有可能向后移,有可能向前移
            let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el:null; // 获取下一个元素
            el.insertBefore(childEl, anchor); // anchor为null被认为是appendChild
            //el.appendChild(childEl);
        }
    }

    if (oldStartIndex <= oldEndIndex) { // 老的需要删除
        for (let i = oldEndIndex; i <= oldEndIndex; i++) {
            let childEl = oldChildren[i].el;
            el.removeChild(childEl);
        }
    }
}

上文是新增节点,会走以上逻辑的尾尾比较:

假设 西瓜a 葡萄b 冰镇汽水c 柠檬d
老节点:a b c
新节点:d a b c
第一次比较: a b,d a b
第二次比较: a,d a
第二次比较: ,d

新节点的头指针 >= 新节点的尾指针,说明有多余的元素,把多余元素插入。这个是有key的情况,可以把字母abcd看成key。如果是无key,会被认为是同一个节点。比如柠檬和西瓜被认为是同一个节点,复用西瓜,然后把柠檬的属性给西瓜:
在这里插入图片描述
同一个节点,比较属性:

export function patchProps(el,oldProps = {}, props={}) {
    // 老的属性中有,新的没有 要删除老的
    let oldStyles = oldProps.style || {};
    let newStyles = props.style || {};

    for (let key in oldStyles) { // 老的样式没有要删除
        if (!newStyles[key]) {
            el.style[key] = ''
        }
    }
    for (let key in oldProps) { // 老的属性没有要删除
        if (!props[key]) {
            el.removeAttribute(key);
        }
    }
    for (let key in props) { // 用新的覆盖老的
        if(key === 'style') {
            for (let styleName in props.style) {
                el.style[styleName] = props.style[styleName]
            }
        } else {
            el.setAttribute(key, props[key])
        }
    }
}

如果key是索引,那新增加的柠檬索引变为0,所以同理,老节点的西瓜和新增加的柠檬会被认为是同一个节点。

总结

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值