Skip to content

基础知识

Key 的作用

Vue列表组件中写 key,其作用是什么
  • 如果没有key
    • Vue 认为这是两个不同的组件,会重新创建组件实例,状态会丢失,并且会更消耗浏览器性能.
  • 书写key的作用主要有两个:
    1. 性能更高 通过key避免不必要的 DOM 更新从而提高渲染性能
      Vue 会根据key的值来判断列表项目是否发生变化,从而决定是否需要重新渲染该列表项的 DOM 结构.
      如果没有keyVue 只能从头开始对列表的每个项目进行 diff,进而决定是否需要重新渲染.这种情况下,尽管数据未发生变化,列表也会进行重新渲染,效率非常低.
      而有了key后,Vue 会根据key值找出两个列表中相同的项目,进而避免不必要的 DOM 更新.
    2. 可以保持组件状态,避免重新渲染时状态丢失.
      当列表进行重新渲染时,如果key值不变,Vue 会认为它是同一个组件,仅会更新该组件,而不会重新创建组件实例.
      这意味着组件的状态会被保留下来,比如输入框的内容、复选框的选中状态等会被保留.

key源码中的体现

首先要知道一点的是,Key 函数在 vue 进行 dom diff 算法的时候对其 dom 的重新渲染加速处理的.所以想要知道 key 在其中的作用,我么就要看 key 在源码中使用时候是怎么来进行调用处理的.

Vue22.6.142.7 以后版本仅添加了关于 vue3 的部分写法支撑,所以可以说 2.6.14 是 vue2 的最后一个版本

dom diff 源码位置 /src/core/vdom/patch.js 点击查看源码

主要位置是 updateChildren 函数中的 while 循环中的最后一个 else 中 代码解释如下

因为没有 key 值导致的性能损耗位置我会用红色注释标记
下面关键引用方法 点击展开
javascript
isUndef;
// 为 undefined 和 null
function isUndef(v) {
  return v === undefined || v === null;
}
// 不为 undefined 和 null
function isDef(v) {
  return v !== undefined && v !== null;
}
// 该方法映射一个 key 到 原来 index 的对象
function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key;
  const map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
}
// 遍历查找当前的index值
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i];
    if (isDef(c) && sameVnode(node, c)) return i;
  }
}
// 判断在当前两个dom节点是否一致
function sameVnode(a, b) {
  return (
    // 如果有key将可以直接比较出不同不用在进行 && 之后的判断
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
  );
}
javascript
else {
  // oldKeyToIdx 是一个map对象 通过 createKeyToOldIdx 方法 return 出来的
  // 当 isUndef(oldKeyToIdx) 为真 也就是第一次进来的时候会触发当前方法
  if (isUndef(oldKeyToIdx))
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  idxInOld = isDef(newStartVnode.key)
    ? oldKeyToIdx[newStartVnode.key]
    // 就是 isDef(newStartVnode.key) 为真 将会调用 findIdxInOld
    // 将会遍历查找当前阶段要处理的所有节点去比对找相等
    : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
  if (isUndef(idxInOld)) { // New element
    // 如果没有key将不可能进入到当前判断中
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm,
     false, newCh, newStartIdx)
  } else {
    vnodeToMove = oldCh[idxInOld]
    // 在sameVnode判断中key也有一部分影响
    if (sameVnode(vnodeToMove, newStartVnode)) {
      patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue,
      newCh, newStartIdx)
      oldCh[idxInOld] = undefined
      canMove && nodeOps.insertBefore(parentElm,
      vnodeToMove.elm, oldStartVnode.elm)
    } else {
      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm,
       false, newCh, newStartIdx)
    }
  }
  newStartVnode = newCh[++newStartIdx]
}

总结下来就是 这个影响是方方面面的 请自行总结.

Vue33.3.4

dom diff 源码位置 /vuejs/core/blob/main/packages/runtime-core/src/renderer.ts 点击查看源码

Vue3 的 Key 值影响比较直观

分为两个方法 patchUnkeyedChildrenpatchKeyedChildren 来实现 dom 的更新操作.

patchUnkeyedChildren 方法中直接就是最简单的遍历循环

  • 长度一致就原地patch 更新(不重新渲染 dom)
  • 新比旧短了就执行unmountChildren 卸载(删除 dom)
  • 新比旧长了就执行mountChildren 挂载(添加 dom)
    • 没有经过任何dom diff算法加速处理.

patchKeyedChildren 方法中 使用了dom diff算法

  • 进行头尾比较
  • 判断是否是仅添加或者仅删除
  • 最后在进行复杂处理
    • 通过二分查找贪心算法实现最长递增子序列
    • 然后以最小的 dom 操作来更新 dom.