从源码解释为什么v-for要加key

阅读本文前请先了解组件更新原理 “组件更新-新旧节点相同”的部分

我们的用例如图,可以新添一项,并可选择checkbox,代码如下:

<div id="app">
      <div>
        <input type="text" v-model="name" />
        <button @click="add">添加</button>
      </div>
      <ul>
        <li v-for="(item, i) in list">
          <input type="checkbox" /> {{item.name}}
        </li>
      </ul>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data: {
          name: "",
          newId: 3,
          list: [
            { id: 1, name: "李斯" },
            { id: 2, name: "吕不韦" },
            { id: 3, name: "嬴政" },
          ],
        },
        methods: {
          add() {
            //注意这里是unshift
            this.list.unshift({ id: ++this.newId, name: this.name });
            this.name = "";
          },
        },
      });
    </script>
 

页面长这样,先选中一项

Xnip2021-05-15_19-54-11.jpg

再添加一项“妲己”

Xnip2021-05-15_19-54-49.jpg
会发现之前被选中的“李斯”没有选中,而新添加的“妲己”被选中了。

在源码src/core/vdom/patch.js中,重要的方法看两个patchVnode,updateChildren,dom树的递归比较和更新就是这两个方法。

当比较ul的新老元素时,进入555行,isDef(oldCh) && isDef(ch),发现旧ul的children和新ul的children都存在,且不一样(每一次更新组件,都会执行render方法,重新创建vnode,所以肯定是不一样的vnode对象),执行updateChildren方法。

这个时候我们看oldCh和ch都是什么值

旧ul vnode的children是3个li vnode

Xnip2021-05-15_20-01-46.jpg

新ul vnode的children是4个li vnode,新加了一个“妲己”

Xnip2021-05-15_20-01-57.jpg

li下面有两个子元素, checkbox和一个文本节点item.name

Xnip2021-05-15_20-03-37.jpg

旧li的子元素 checkbox值“oldCh[0].children[0].checked”为true,因为被勾选了,而新li的子元素的checkbox值还不存在,所以是undefined,还没有创建真实dom元素,还是虚拟dom状态。

Xnip2021-05-15_20-38-02.jpg

现在进入updateChildren方法看看。
执行到430行,oldStartVnode和newStartVnode,

Xnip2021-05-15_21-51-30.jpg

oldStartVnode代表旧ul的children的第一个虚拟节点li vnode;newStartVnode代表新ul的children的第一个虚拟节点li vnode。

这里有一个判断,sameNode方法, 正是因为v-for没有加key,就会认为新旧两个li的vnode是相同的,所以又会执行patchNode。这个时候又会进入patchNode方法。

然后又会执行到555行,判断li的children又不相同,又进入updateChildren,参数是li的children数组,分别是checkbox和item.name两个虚拟节点。

所以vnode的递归比较和更新就是这两个方法。更新在patchNode,children的比较顺序在updateChildren。

又执行到430行,分别判断第一个子元素oldStartVnode和newStartVnode,两个checkbox还是相同,又进入patchNode,这个时候执行到518行。

Xnip2021-05-15_22-19-17.jpg

会把旧的checkbox真实dom对象赋值给新checkbox vnode的elm。所以在渲染到真实dom树的时候,第一个li标签,也就是“妲己”,里面的子元素checkbox就是选中的。

继续往下执行,没有改变dom值的操作,都是hook,回调。其实真正修改真实dom也是在这个方法中,像addVnodes,removeVnodes,setTextContent。

执行完之后回到上一个方法-updateChildren,还记得while循环吗?就是轮循去一个一个比较children,比较完li下面的checkbox,现在比较文本节点item.name。依然根据比较规则,会执行430行,又进入patchNode,这次执行到569行,因为是一个文本节点,在上面的判断isUndef(vnode.text),这是一个文本节点,是定义了的,所以会走else if分支。因为“李斯”和“妲己”不相同,所以会替换掉“李斯”,改为“妲己”。

所以就出现了现在新增的“妲己”是选中了的。

Xnip2021-05-16_09-17-49.jpg

在比较第二个li vnode的children的时候,也是同样的道理,原来的第二个li的checkbox没有被选中,因为判断是同一个节点,旧checkbox vnode的elm会赋值给新checkbox vnode的elm。

现在如果加上key,以id为key,会发生什么?
因为在比较li的时候会用sameNode判断是否为相同的节点,其中一个比较是key,

Xnip2021-05-16_09-29-45.jpg
会判断不是相同节点,就会执行不相同节点的操作,“创建新节点 -> 更新占位符节点 -> 删除旧节点的逻辑”,执行718行else分支。

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/14739.html

发表评论

登录后才能评论