# Vue3 Diff 算法与key

Vue3 Diff 算法与key

先来看看官方的解释

  • key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;

    • 如果不使用Key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法;

    • 而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;

  • 官方的解释对于初学者来说并不好理解,比如下面的问题:

    • 什么是新旧nodes,什么是VNode?

    • 没有key的时候,如何尝试修改和复用的?

    • 有key的时候,如何基于key重新排列的?

认识VNode

  • VNode的全称是Virtual Node,也就是虚拟节点;

  • 事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode;

  • VNode的本质是一个JavaScript的对象;

    <div class="title" style="font-size:30px;color:pink;">
        哈哈哈
    </div>
    
    const vnode = {
        type:"div",
        props:{
            class:"title",
            style:{
                "font-size":"30px",
                color:"pink",
            },
        },
        children:"哈哈哈"
    }
    
  • 如果我们不只是一个简单的div,而是有一大堆元素,那么它们应该会形成以一个VNode Tree;

    <div>
        <p>
            <i>哈哈哈</i>
            <i>哈哈哈</i>
        </p>
        <span>嘻嘻嘻嘻</span>
        <strong>呵呵呵呵</strong>
    </div>
    

在这里插入图片描述

  • 然后我们来看一个案例:这个按钮是当我们点击按钮时会在中间插入一个f

    <div id="app">
        
    </div>
    <template id="my-app">
    	<ul>
            <li v-for="item in letters">{{item}}</li>
        </ul>
        <button @click="insertF">insert f</button>
    </template>
    <script src="https://unpkg.com/vue@next"></script>
    <script>
    	const App = {
            template:"#my-app",
            data() {
                return {
                    letters: ["a","b","c","d"]
                }
            },
            methods:{
                insertF() {
                    this.letters.insert(2,0,"f");
                }
            }
        }
        Vue.create(App).mount("#app")
    </script>
    
    • 我们可以确定的是,这次跟新对于ulbutton是不需要进行更新,需要更新的是我们li的列表:

      • 在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列表;
      • 因为对于列表中a、b、c、d它们都是没有变化的;
      • 在操作真实DOM的时候,我们只需要在中间插入一个f的li即可
    • 那么Vue中对于列表的更新究竟是如何操作的呢?

      • Vue事实上会对于有key和没有key会调用两个不同的方法;
      • 有key,那么就使用patchKeyedChildren方法;
      • 没有key,那么就使用patchUnkeyedChildren方法;
    • 没有key的diff算法(patchUnkeyedChildren)

        const patchUnkeyedChildren = (
          c1: VNode[],
          c2: VNodeArrayChildren,
          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
          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,
              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
            )
          }
        }
      
      • 首先是拿到新 旧VNode并拿到长度较小的数组
      • 然后从0位置开始依次对新,旧数组中的每一个元素进行patch比较,如果没有差异就不做更新
      • 如果类型有差异直接创建新的类型,如果类型一样内容有差异就更新内容。
      • 如果旧VNode的长度大于新VNode的长度,直接unmount旧数组多余的元素。
      • 如果新VNode的长度大于旧VNode的长度,生成新的VNode挂载上去。
    • 有key的diff算法(patchKeyedChildren)

      • 第一步操作是从头开始进行遍历、比较

        • a和b是一致的会继续进行比较;
        • c和f因为key不一致,所以就会break跳出循环;
          在这里插入图片描述
      • 第二步操作是从尾部开始进行遍历、比较
        在这里插入图片描述

      • 第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:
        在这里插入图片描述

      • 第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么移除旧节点:

        在这里插入图片描述

      • 第五步是特殊情况,中间还有很多未知的或者乱序的节点:
        在这里插入图片描述

         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)) {
              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
          // (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
            const keyToNewIndexMap: Map<string | number | symbol, 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
            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--
                }
              }
            }
          }
        }
      
  • 所以我们可以发现,Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作:

    • 在没有key的时候我们效率是非常低效的;
    • 在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值