likes
comments
collection
share

vue3 key对diff的影响

作者站长头像
站长
· 阅读数 4

前言

这是vue3系列源码的第十一章,使用的vue3版本是3.4.15

背景

这一节中,我们看一下vue3diff的实现,以及keydiff过程的影响

前置

// app.vue
<template>
  <div>
    <button @click="check">点击</button>
  </div>
  <div>
    <textCom :data="item" v-for="(item, index) in aa"/>
  </div>
 </template>
 <script setup>
 import { ref} from 'vue' 
 import textCom from './text.vue'

 const aa = ref([1,2,3])
 const check = () => {
  aa.value = [2,1,3]
 }
 </script>

app组件中对一个数组进行循环,渲染子组件。

这里的text组件只需要接收一个传进来的props,并渲染一下

// text.vue
<template>
<div>{{ data }}</div>
</template>
<script setup>
  defineProps({
    data: Number
  })
</script>

没有key

首先我们按照上面的写法,就是循环的时候没有设置key, 看一下,这个时候的更新过程。

patchChildren

  const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    slotScopeIds,
    optimized = false,
  ) => {
          const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children

    const { patchFlag, shapeFlag } = n2
    // fast path
    if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
        // this could be either fully-keyed or mixed (some keyed some not)
        // presence of patchFlag means children are guaranteed to be arrays
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
        // unkeyed
        patchUnkeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        return
      }
    }
    ...
  }

patchChildren函数中,主要是针对了不同的情况,进行了不同的patch处理。

在我们的这个例子中,它会先根据有没有绑定key进行判断,我们这里没有绑定key,进入patchUnkeyedChildren函数。

patchUnkeyedChildren

const patchUnkeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length
    const newLength = c2.length
    const commonLength = Math.min(oldLength, newLength)
    let i
    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,
        namespace,
        slotScopeIds,
        optimized,
      )
    }
    if (oldLength > newLength) {
      // remove old
      unmountChildren(
        c1,
        parentComponent,
        parentSuspense,
        true,
        false,
        commonLength,
      )
    } else {
      // mount new
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
        commonLength,
      )
    }
  }

这个函数其实就干了两件事情:

  • 依次按顺序遍历新老节点,进行patch
  • 对多出来的老节点进行unmount,对多出来的新节点进行mount

所以我们可以看见,当我们不绑定key的时候,其实更新过程没有涉及到diff算法,就是一种暴力的更新过程。

那么下面,我们看看绑定了key的过程。

key 为 index

我们首先看一下我们在开发中可能的一种写法,就是把index绑定为key

我们先改造一下app.vue

 <textCom :data="item" v-for="(item, index) in aa" :key="index"/>

这次,我们绑定了key,在patchChildren函数中,走到了patchKeyedChildren函数中。

patchKeyedChildren

这个函数就是diff过程的核心函数,vue3的diff算法就是这个函数。

  // can be all-keyed or mixed
  const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    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)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      } else {
        break
      }
      i++
    }
    ...
  }

diff过程针对不同的情况,做了不同的处理,我们一点一点看。

isSameVNodeType

这里我们要先看一下工具函数,判断新旧节点是否相同类型。

function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as ConcreteComponent)
  ) {
    // #7042, ensure the vnode being unmounted during HMR
    // bitwise operations to remove keep alive flags
    n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
    n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
    // HMR only: if the component has been hot-updated, force a reload.
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}

这里主要的判断依据就是typekey

那么type是什么:

vue3  key对diff的影响

type其实是一个包含了几个属性的对象,基本上描述了一个组件的全部内容。

那么key就是我们绑定的key.

回到我们这个例子中,当我们用index来绑定key的时候,我们会发现,虽然数组变了,但是新旧节点的key是没有变化的。

所以,在isSameVNodeType函数的判断中,新旧节点是符合条件的,那么就会直接进入patch

一直到遍历完新旧节点数量少的那个为止。


    // 3. common sequence + 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,
            namespace,
            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++
      }
    }

最后,就和没有绑定key的过程一样,新节点少的,进行unmount,新节点多出来的,进行patch

那么,我们可以得出一个结论:

当我们用数组的index作为key的时候,在遇到像排序这种变化的时候,和没有绑定key是一样的效果。

id 为 key

下面我们再看看,我们不用indexkey,用唯一标志符作为key,比如iddiff过程是什么样的。

这里,我们再次改造一下app.vue文件

<textCom :data="item" v-for="(item, index) in aa" :key="item"/>
...
const check = () => {
  aa.value = [3,1,2,4]
 }

这里,我们用item本身代替id

此时,由于第一项的key不一样,不满足isSameVNodeType的条件,所以直接break出来,进入下一个判断。

// 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,
          namespace,
          slotScopeIds,
          optimized,
        )
      } else {
        break
      }
      e1--
      e2--
    }

上面是第二个判断,从两边的尾部开始对比,如果是是sameVnode,进行patch操作, 这里仍然不符合

    // 3. common sequence + 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,
            namespace,
            slotScopeIds,
            optimized,
          )
          i++
        }
      }
    }

第三个和第四个判断,就是上面keyindex的时候,提到的,当新旧节点数量不相同的时候,就会进行挂载或者卸载操作

如果上面的四个判断都经过了,还是有没有处理掉的节点,那么此时就到了diff算法的核心部分,对更加一般情况的处理。

  • 将存在于旧节点,但是不存在于新节点的节点卸载,新旧都有的节点进行更新
  • 得到一个数组,数组是按照新节点的顺序,保存了老节点的索引+1
  • 找到最长公共子序列
  • 从剩余节点的最后一个往最前一个遍历,遇到新的节点就进行增加
  • 以最长子序列的位置为基准,移动其他节点,做到最大程度的复用

最长公共子序列

这里最难的部分是最长公共子序列的寻找。

这里会首先得到一个数组。

newIndexToOldIndexMap:

newIndexToOldIndexMap[newIndex - s2] = i + 1

其中i代表老节点中的索引。也就是说,这个数组存下了老节点新节点中的顺序。

那么要找到最小的变化,尽可能多的复用节点,就是要找到新老节点之间的最多的连续节点,节点的连续反映到索引上就是索引的递增,所以其实就是在寻找这个数组的最长递增子序列。

这里的核心算法采用了动态规划 + 二分法

可以参考 300.最长递增子序列,但是这里得到的数组只有长度是准确的,实际的元素并不准确

所以源码做了一些修改,保存了真实的元素。

总结

以上就是diff过程的全部内容。我们看了一下diff的各个过程

回到标题,keydiff过程的影响是很大的。

我们讨论了在例子情况下,用index当做key的弊端,有时候和没有加key的效果是一样,这样会加大diff过程的开销,不符合复用原则,而已在有的情况下还会发生错误。

所以一个唯一且固定的key对于vue3来说是很重要的,能很大提升源码运行的效率。

转载自:https://juejin.cn/post/7346580626319654924
评论
请登录