v-for列表渲染有无key值对比-diff算法详解

一、前置知识

官方解读

在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性。那么这个key属性有什么作用呢?我们先来看一下官方的解释:

  • key 这个特殊的 attribute 主要作为 Vue 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。
  • 在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,并尽可能地就地更新/复用相同类型的元素。
  • 如果传了 key,则将根据 key 的变化顺序来重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。
  • 同一个父元素下的子元素必须具有唯一的 key。重复的 key 将会导致渲染异常。

VNode

VNode的全称是Virtual Node,也就是虚拟节点;事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode。VNode的本质是一个JavaScript对象。大概是这个样子。

<div class="title" style="color: red;">Vue</div>
const vnode = {
    type: "div",
    props: {
        class: "title",
        style: {
            color: "red",
        },
    },
    children: "Vue"
}

二、一个小例子

想象这样一个例子,v-for渲染一个包含[ ‘a’, ‘b’, ‘c’ ]的数组,然后定义一个方法,可以往’b’后插入’d’。

我们可以先想一下要进行上面的操作的话,可以有几种实现方式。

全部干掉——完全不推荐

即将原先的内容全部删除,然后再重新生成一个新数组再进行渲染。

修改部分——也不推荐

这个算法其实是vue中对没key时的一个处理,所以这里我就不再赘述,下面会有演示。


了解了上面那些东西之后,你就可以相对比较轻松地学习接下来的内容了。

三、diff算法

新旧vnode对比以达到尽可能地复用地这个算法就叫做diff。

而Vue对有key和没key会调用两种不同的方法;有key,那么就使用patchKeyedChildren方法;没key,就使用patchUnkeyedChildren方法。

如果你有下载vue3源码的话,可以在下载后的代码的‘packages\runtime-core\src\renderer.ts’中找到对应的代码,不过没有下载也不影响你接下来的阅读。

在这里插入图片描述


此时vue内部会对是否有key进行判断,上图中已经给出判断的具体位置。

无key

因为vue3是用typescript写的,所以可能有些代码你看不懂,但是逻辑上还是挺容易理解的,应该不影响你继续阅读。

如果你懒得看源码的话,我可以大致说一下做了什么事情:

  1. 首先对新旧节点的长度进行比较,然后获取最小的那个长度
  2. 从0位置开始进行patch比较,直到最小长度的节点遍历完成
  3. 如果旧的节点比新的长,则直接把多余的节点删除
  4. 如果旧的节点比新的短,则把新节点添加进来,即创建新的节点
  const patchUnkeyedChildren = (
    c1: VNode[], // 旧nodes ['a', 'b', 'c', 'd']
    c2: VNodeArrayChildren, // 旧nodes ['a', 'b', 'f', 'c', 'd']
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    // 1.获取旧节点的长度
    const oldLength = c1.length
    // 2.获取新节点的长度
    const newLength = c2.length
    // 获取最小的那个长度
    const commonLength = Math.min(oldLength, newLength)
    let i
    // 从0位置开始依次patch比较
    for (i = 0; i < commonLength; i++) {
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
    // 如果旧的节点数大于新的节点数
    if (oldLength > newLength) {
      // remove old
      // 移除剩余的节点
      unmountChildren(
        c1,
        parentComponent,
        parentSuspense,
        true,
        false,
        commonLength
      )
    } else {
      // mount new
      // 创建新的节点
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized,
        commonLength
      )
    }
  }

有key

有key的diff算法比没key的要复杂一点,为了避免你看源码时一头雾水,我先讲一下大概的实现思路:

  1. 首先计算出新旧节点的长度,然后从头部开始遍历,如果节点相同,那么就继续遍历。如果不同就直接跳出循环
  2. 根据前面计算出来的长度,从尾部开始遍历,如果节点相同,那么就继续遍历,不同就直接跳出循环
  3. 如果旧节点遍历完了,依然有新的节点,那么新的节点就是添加
  4. 第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点
  5. 第五步是最特色的情况,中间还有很多未知的或者乱序的节点:
  const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 // prev ending index
    let e2 = l2 - 1 // next ending index

    // 1. sync from start
    // 从头部开始遍历
    // (a b) c
    // (a b) d e
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      // 如果节点相同, 那么就继续遍历
      if (isSameVNodeType(n1, n2)) { // 这里isSameVNodeType这个函数所做的操作是比较n1和n2的type和key是否相同
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // 节点不同就直接跳出循环
        break
      }
      i++
    }

    // 2. sync from end
    // 从尾部开始遍历
    // a (b c)
    // d e (b c)
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      // 如果节点相同, 就继续遍历
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // 不同就直接跳出循环
        break
      }
      e1--
      e2--
    }

    // 3. common sequence + mount
    // 如果旧节点遍历完了, 依然有新的节点, 那么新的节点就是添加(mount)
    // (a b)
    // (a b) c
    // i = 2, e1 = 1, e2 = 2
    // (a b)
    // c (a b)
    // i = 0, e1 = -1, e2 = 0
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch(
            null,
            (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          i++
        }
      }
    }

    // 4. common sequence + unmount
    // 如果新的节点遍历完了, 还有旧的节点, 那么旧的节点就是移除的
    // (a b) c
    // (a b)
    // i = 2, e1 = 2, e2 = 1
    // a (b c)
    // (b c)
    // i = 0, e1 = 0, e2 = -1
    else if (i > e2) {
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
      }
    }

    // 5. unknown sequence
    // 如果是位置的节点序列,
    // 如果有多余的节点, 那么就移除节点
    // 之后是移动节点和挂载新节点
    // [i ... e1 + 1]: a b [c d e] f g
    // [i ... e2 + 1]: a b [e d c h] f g
    // i = 2, e1 = 4, e2 = 5
    else {
      const s1 = i // prev starting index
      const s2 = i // next starting index

      // 5.1 build key:index map for newChildren
      // 根据key建立map索引图
      const keyToNewIndexMap: Map<string | number, number> = new Map()
      for (i = s2; i <= e2; i++) {
        const nextChild = (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i]))
        if (nextChild.key != null) {
          if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
            warn(
              `Duplicate keys found during update:`,
              JSON.stringify(nextChild.key),
              `Make sure keys are unique.`
            )
          }
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }

      // 5.2 loop through old children left to be patched and try to patch
      // matching nodes & remove nodes that are no longer present
      // 遍历剩下的旧节点, 新旧对比, 移除不使用的旧节点
      let j
      let patched = 0
      const toBePatched = e2 - s2 + 1
      let moved = false
      // used to track whether any node has moved
      let maxNewIndexSoFar = 0
      // works as Map<newIndex, oldIndex>
      // Note that oldIndex is offset by +1
      // and oldIndex = 0 is a special value indicating the new node has
      // no corresponding old node.
      // used for determining longest stable subsequence
      const newIndexToOldIndexMap = new Array(toBePatched)
      for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

      for (i = s1; i <= e1; i++) {
        const prevChild = c1[i]
        if (patched >= toBePatched) {
          // all new children have been patched so this can only be a removal
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else {
          // key-less node, try to locate a key-less node of the same type
          for (j = s2; j <= e2; j++) {
            if (
              newIndexToOldIndexMap[j - s2] === 0 &&
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) {
              newIndex = j
              break
            }
          }
        }
        if (newIndex === undefined) {
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
          newIndexToOldIndexMap[newIndex - s2] = i + 1
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else {
            moved = true
          }
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          patched++
        }
      }

      // 5.3 move and mount
      // generate longest stable subsequence only when nodes have moved
      // 拿到最长递增子序列进行move 和 mount
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      // looping backwards so that we can use last patched node as anchor
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) {
          // mount new
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (moved) {
          // move if:
          // There is no stable subsequence (e.g. a reverse)
          // OR current node is not among the stable sequence
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }
        }
      }
    }
  }

理解了上面的内容之后,你也就明白了为什么使用v-for遍历时要添加key作为标识,而且不推荐使用index作为key。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值