Code前端首页关于Code前端联系我们

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

terry 2年前 (2023-09-08) 阅读数 158 #Vue

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

我们的用例如图所示,您可以添加新项目并且可以选中复选框,代码如下:

<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.jpgXnip2021-05-15_19-54-11.jpg

再加一个“妲己”

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

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

在src/core/vdom/patch.js源码中,两个patchVnode、updateChildren、递归比较和更新dom树都是重要的方法。 比较新旧ul元素时,进入第555行,isDef(oldCh) && isDef(ch),发现old ul的孩子和new ul的孩子存在并且不同(每次,如果组件更新,执行render方法,重新创建vnode,所以它必须是另一个vnode对象),运行updateChildren方法。 此时我们看一下oldCh和ch的值

老ulvnode的孩子是3里vnode

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

ulvnode的新孩子有4里vnode,又增加了一个新的“妲己”

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

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

如果它下面有两个子元素,一个复选框和一个文本节点 item.name

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

旧li的孩子的“oldCh[0].children[0].checked”复选框值为true,因为它被选中,而新li的孩子的checkbox值还不存在,所以它是未定义的。不会创建真实圆顶元素或虚拟圆顶状态。

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

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

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

oldStartVnode 表示old ul 的第一个虚拟节点li vnode 孩子; newStartVnode代表new街孩子们的第一个虚拟节点li vnode

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

有一个判决,sameNode方法,只是因为v-for没有添加key,它会认为新旧li的王牌相同,所以会再次执行patchNode。此时,又重新进入patchNode方法。

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

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

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

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

在第 430 行执行,并对 oldStartVnode 和 newStartVnode 的第一个子元素进行求值。两个复选框还是一样,然后输入patchNode。目前,执行正在发送到第 518 行。

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

将旧复选框对象的真实房屋分配给新的 vnode Elm 复选框。所以渲染到实际的DOM树时,会选中第一个li标签,即“妲己”,并且选中里面的子元素的复选框。

继续执行,没有任何改变dom值的操作,都是钩子和回调。其实实际房子的修改也在这个方法中,比如addVnodes、removeVnodes、setTextContent等。

执行完毕后,返回到上一个方法——updateChildren。还记得 while 循环吗?重点是将孩子们一一匹配成一个圆圈。比较完 li 下的复选框后,现在比较 item_name 文本节点。还是按照比较规则,执行到第430行,然后进入patchNode,这次到了第569行,由于这是一个文本节点,所以上面的判断是Undef(vnode.text),这是一个文本节点,是有定义的,所以else if 分支将被删除。由于“李斯”和“妲己”不一样,所以“李斯”将被“妲己”取代。

于是新添加的“妲己”出现并被选中。

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

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

对比第二任vnode的孩子,也是同样的道理。原来第二个li的复选框没有被选中,因为判断是同一个节点,旧的复选框vnode的elm会被赋值给新的复选框vnode的elm。

现在当你添加一个键并使用 id 作为键时,会发生什么?
因为比较的时候用if sameNode来判断是否是同一个节点。其中一个明喻是关键,

Xnip2021-05-16_09-29-45.jpgXnip2021-05-16_09-29-45.jpg
会判断不是同一个节点,执行不同的节点操作,“创建新节点->更新代理节点->删除旧节点逻辑”并执行第718行else分支。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门