前言
Vue 中的 key 是用来做什么的?为什么不推荐使用 index 作为 key?常常听说这样的问题,本篇文章带你从原理来一探究竟。
另外本文的结论对于性能的毁灭是针对列表子元素顺序会交换、或者子元素被删除的特殊情况,提前说明清楚,喷子绕道。
本篇已经收录在 Github 仓库,欢迎 Star:
https://github.com/sl1673495/blogs/issues/39
示例
以这样一个列表为例:
<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__ 过程。
我们直接进入 ul 这个 vnode 的 patch 过程。
对比新旧节点是否是相同类型的节点:
1. 不是相同节点:
isSameNode为false的话,直接销毁旧的 vnode,渲染新的 vnode。这也解释了为什么 diff 是同层对比。
2. 是相同节点,要尽可能的做节点的复用(都是 ul,进入"htmlcode">
 这些变量分别指向旧节点的首尾、新节点的首尾。 根据这些指针,在一个 while 循环中不停的对新旧节点的两端的进行对比,直到没有节点可以对比。 在讲对比过程之前,要讲一个比较重要的函数:sameVnode: 它是用来判断节点是否可用的关键函数,可以看到,判断是否是 sameVnode,传递给节点的 key 是关键。 然后不停的把匹配到的指针向内部收缩,直到新旧节点有一端的指针相遇(说明这个端的节点都被patch过了)。 为什么不要以index作为key? 节点reverse场景 假设我们有这样的一段代码: 其实是一个很简单的列表组件,渲染出来 1 2 3 三个数字。我们先以 index 作为key,来跟踪一下它的更新。 我们接下来只关注 item 列表节点的更新,在首次渲染的时候,我们的虚拟节点列表 oldChildren 粗略表示是这样的: 在我们点击按钮的时候,会对数组做 reverse 的操作。那么我们此时生成的 newChildren 列表是这样的: 发现什么问题没有?key的顺序没变,传入的值完全变了。这会导致一个什么问题? 本来按照最合理的逻辑来说,旧的第一个vnode 是应该直接完全复用 新的第三个vnode的,因为它们本来就应该是同一个vnode,自然所有的属性都是相同的。 但是在进行子节点的 diff 过程中,会在 旧首节点和新首节点用sameNode对比。 这一步命中逻辑,因为现在新旧两次首部节点 的 key 都是 0了, 然后把旧的节点中的第一个 vnode 和 新的节点中的第一个 vnode 进行 patchVnode 操作。 这会发生什么呢?我可以大致给你列一下: 首先,正如我之前的文章props的更新如何触发重渲染?里所说,在进行 patchVnode 的时候,会去检查 props 有没有变更,如果有的话,会通过 _props.num = 3 这样的逻辑去更新这个响应式的值,触发 dep.notify,触发子组件视图的重新渲染等一套很重的逻辑。 然后,还会额外的触发以下几个钩子,假设我们的组件上定义了一些dom的属性或者类名、样式、指令,那么都会被全量的更新。 而这些所有重量级的操作(虚拟dom发明的其中一个目的不就是为了减少真实dom的操作么?),都可以通过直接复用 第三个vnode 来避免,是因为我们偷懒写了 index 作为 key,而导致所有的优化失效了。 节点删除场景 另外,除了会导致性能损耗以外,在删除子节点的场景下还会造成更严重的错误, 可以看sea_ljf同学提供的这个demo。 假设我们有这样的一段代码: 那么一开始的 vnode列表是: 有一个细节需要注意,正如我上一篇文章中所提到的为什么说 Vue 的响应式更新比 React 快?,Vue 对于组件的 diff 是不关心子组件内部实现的,它只会看你在模板上声明的传递给子组件的一些属性是否有更新。 也就是和v-for平级的那部分,回顾一下判断 sameNode 的时候,只会判断key、 tag、是否有data的存在(不关心内部具体的值)、是否是注释节点、是否是相同的input type,来判断是否可以复用这个节点。 有了这些前置知识以后,我们来看看,点击删除子元素后,vnode 列表 变成什么样了。 虽然在注释里我们自己清楚的知道,第一个 vnode 被删除了,但是对于 Vue 来说,它是感知不到子组件里面到底是什么样的实现(它不会深入子组件去对比文本内容),那么这时候 Vue 会怎么 patch 呢? 由于对应的 key使用了 index导致的错乱,它会把 至此为止,我们本应该把 text: 1节点删掉,然后text: 2、text: 3 节点复用,就变成了错误的把 text: 3 节点给删掉了。 为什么不要用随机数作为key? 其实我听过一种说法,既然官方要求一个 唯一的key,是不是可以用 Math.random() 作为 key 来偷懒?这是一个很鸡贼的想法,看看会发生什么吧。 首先 oldVnode 是这样的: 更新以后是: 可以看到,key 变成了完全全新的 3 个随机数。 上面说到,diff 子节点的首尾对比如果都没有命中,就会进入 key 的详细对比过程,简单来说,就是利用旧节点的 key -> index 的关系建立一个 map 映射表,然后用新节点的 key 去匹配,如果没找到的话,就会调用 createElm 方法 重新建立 一个新节点。 具体代码在这: 也就是说,咱们的这个更新过程可以这样描述: 123 -> 前面重新创建三个子组件 -> 321123  -> 删除、销毁后面三个子组件 -> 321。 总结 经过这样的一段旅行,diff 这个庞大的过程就结束了。 我们收获了什么?
 // 旧首节点
 let oldStartIdx = 0
 // 新首节点
 let newStartIdx = 0
 // 旧尾节点
 let oldEndIdx = oldCh.length - 1
 // 新尾节点
 let newEndIdx = newCh.length - 1
function sameVnode (a, b) {
 return (
  a.key === b.key && (
   (
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
   )
  )
 )
}
然后我们接着进入 diff 过程,每一轮都是同样的对比,其中某一项命中了,就递归的进入 patchVnode 针对单个 vnode 进行的过程(如果这个 vnode 又有 children,那么还会来到这个 diff children 的过程 ):
  
在指针相遇以后,还有两种比较特殊的情况:
  
  <div id="app">
   <ul>
    <item
     :key="index"
     v-for="(num, index) in nums"
     :num="num"
     :class="`item${num}`"
    ></item>
   </ul>
   <button @click="change">改变</button>
  </div>
  <script src="/UploadFiles/2021-04-02/vue.js">
[
 {
  tag: "item",
  key: 0,
  props: {
   num: 1
  }
 },
 {
  tag: "item",
  key: 1,
  props: {
   num: 2
  }
 },
 {
  tag: "item",
  key: 2,
  props: {
   num: 3
  }
 }
];
[
 {
  tag: "item",
  key: 0,
  props: {
+   num: 3
  }
 },
 {
  tag: "item",
  key: 1,
  props: {
+   num: 2
  }
 },
 {
  tag: "item",
  key: 2,
  props: {
+   num: 1
  }
 }
];
  
<body>
 <div id="app">
  <ul>
   <li v-for="(value, index) in arr" :key="index">
    <test />
   </li>
  </ul>
  <button @click="handleDelete">delete</button>
 </div>
 </div>
</body>
<script>
 new Vue({
  name: "App",
  el: '#app',
  data() {
   return {
    arr: [1, 2, 3]
   };
  },
  methods: {
   handleDelete() {
    this.arr.splice(0, 1);
   }
  },
  components: {
   test: {
    template: "<li>{{Math.random()}}</li>"
   }
  }
 })
</script>
[
 {
  tag: "li",
  key: 0,
  // 这里其实子组件对应的是第一个 假设子组件的text是1
 },
 {
  tag: "li",
  key: 1,
  // 这里其实子组件对应的是第二个 假设子组件的text是2
 },
 {
  tag: "li",
  key: 2,
  // 这里其实子组件对应的是第三个 假设子组件的text是3
 }
];
<li v-for="(value, index) in arr" :key="index"> // 这里声明的属性
 <test />
</li>
[
 // 第一个被删了
 {
  tag: "li",
  key: 0,
  // 这里其实上一轮子组件对应的是第二个 假设子组件的text是2
 },
 {
  tag: "li",
  key: 1,
  // 这里其实子组件对应的是第三个 假设子组件的text是3
 },
];
  
<item
 :key="Math.random()"
 v-for="(num, index) in nums"
 :num="num"
 :class="`item${num}`"
/>
[
 {
  tag: "item",
  key: 0.6330715699108844,
  props: {
   num: 1
  }
 },
 {
  tag: "item",
  key: 0.25104533240710514,
  props: {
   num: 2
  }
 },
 {
  tag: "item",
  key: 0.4114769152411637,
  props: {
   num: 3
  }
 }
];
[
 {
  tag: "item",
+  key: 0.11046018699748683,
  props: {
+   num: 3
  }
 },
 {
  tag: "item",
+  key: 0.8549799545696619,
  props: {
+   num: 2
  }
 },
 {
  tag: "item",
+  key: 0.18674467938937478,
  props: {
+   num: 1
  }
 }
];
// 建立旧节点的 key -> index 映射表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
// 去映射表里找可以复用的 index
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 一定是找不到的,因为新节点的 key 是随机生成的。
if (isUndef(idxInOld)) {
 // 完全通过 vnode 新建一个真实的子节点
 createElm();
}
发现问题了吧?这是毁灭性的灾难,创建新的组件和销毁组件的成本你们晓得的伐……本来仅仅是对组件移动位置就可以完成的更新,被我们毁成这样了。
  
RTX 5090要首发 性能要翻倍!三星展示GDDR7显存
三星在GTC上展示了专为下一代游戏GPU设计的GDDR7内存。
首次推出的GDDR7内存模块密度为16GB,每个模块容量为2GB。其速度预设为32 Gbps(PAM3),但也可以降至28 Gbps,以提高产量和初始阶段的整体性能和成本效益。
据三星表示,GDDR7内存的能效将提高20%,同时工作电压仅为1.1V,低于标准的1.2V。通过采用更新的封装材料和优化的电路设计,使得在高速运行时的发热量降低,GDDR7的热阻比GDDR6降低了70%。
更新动态
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
- 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
- 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
- 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
- 群星《2024好听新歌42》AI调整音效【WAV分轨】
- 王思雨-《思念陪着鸿雁飞》WAV
- 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
- 李健《无时无刻》[WAV+CUE][590M]
- 陈奕迅《酝酿》[WAV分轨][502M]
- 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
- 群星《吉他王(黑胶CD)》[WAV+CUE]
- 齐秦《穿乐(穿越)》[WAV+CUE]
- 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
- 邝美云《邝美云精装歌集》[DSF][1.6G]
- 吕方《爱一回伤一回》[WAV+CUE][454M]
