vue不用index作key(diff算法)

443 阅读2分钟

patchVnode、Vnode请看这篇。

示例

<ul>
  <li>1</li>
  <li>2</li>
</ul>

那么它的vnode也就是虚拟dom节点大概是:

{
  tag: 'ul',
  children: [
    { tag: 'li', children: [ { vnode: { text: '1' }}]  },
    { tag: 'li', children: [ { vnode: { text: '2' }}]  },
  ]
}

假设更新以后,我们把子节点的顺序调换了一下:

{
  tag: 'ul',
  children: [
+   { tag: 'li', children: [ { vnode: { text: '2' }}]  },
+   { tag: 'li', children: [ { vnode: { text: '1' }}]  },
  ]
}

children部分是diff算法的重点。

首先响应式数据更新后,触发渲染Watcher的回调函数vm_update(vm._render())去驱动视图更新,

vm._render()其实生成的就是vnode,而vm._update会带着新的vnode触发__patch__过程。

1. 不是相同节点:

isSameNode为false的话,直接销毁旧的vnode,渲染新的vnode。所以diff是同层对比。

2. 是相同节点,要尽可能做节点的复用

会调用src/core/vdom/patch.js下的patchVNode方法,

如果新 vnode 是文字 vnode

就直接调用浏览器的 dom api 把节点的直接替换掉文字内容就好。

如果新 vnode 不是文字 vnode

那么就要开始对子节点 children 进行对比了。(可以类比 ul 中的 li 子元素)。

如果有新 children 而没有旧 children

说明是新增 children,直接 addVnodes 添加新子节点。

如果有旧 children 而没有新 children

说明是删除 children,直接 removeVnodes 删除旧子节点

如果新旧 children 都存在

核心:新旧节点的diff过程

在新旧节点的两端进行对比,用sameVnode函数比较是否是sameVnode,传递给节点的key是关键。

其中某一项命中了就递归进入patchVnode针对单个vnode进行的过程。

假如进行一个数组reverse的操作

如果把key设置为index

<item
      :key="index"
      v-for="(num, index) in nums"
      :num="num"
      :class="`item${num}`"
></item>

那么绑定点击事件reverse时,key的顺序没有变,传入的值完全变了。

按逻辑,旧的第一个vnode,应该直接完全复用新的第三个vnode,因为本来就是同一个vnode。

但是在进行子节点的 diff 过程中,会在 旧首节点和新首节点用sameNode对比。 这一步命中逻辑,因为现在新旧两次首部节点 的 key 都是 0了,

然后把旧的节点中的第一个 vnode 和 新的节点中的第一个 vnode 进行 patchVnode 操作。

导致性能损耗。

节点删除场景

删除第一个li

由于对应的 key使用了 index导致的错乱,它会把:

  1. 原来的第一个节点text: 1直接复用。
  2. 原来的第二个节点text: 2直接复用。
  3. 然后发现新节点里少了一个,直接把多出来的第三个节点text: 3 丢掉。

总结

  1. 用组件唯一的id作为它的key
  2. 不要用随机数作为key,因为旧节点会被全部删掉,新节点重新创建。