vue3-dom-diff算法

vue3diff算法

什么是vue3diff算法

Vue3中的diff算法是一种用于比较虚拟DOM树之间差异的算法,其目的是为了高效地更新真实DOM,减少不必要的重渲染

主要过程

整个过程主要分为以下五步

  1. 前置预处理
  2. 后置预处理
  3. 仅处理新增
  4. 仅处理后置
  5. 处理包含新增、卸载、移动的复杂场景

第一步:前置预处理

首先定义一个变量i,记录当前索引值

定义e1、e2记录前置索引值

从前到后遍历新旧索引列表

发现它们的节点值相同,则直接patch打补丁更新

否则跳出循环进入下一步。

vue3源码:

javascript 复制代码
    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++
    }

以下demo为例:

i=0 新旧节点都是n1 patch打补丁更新

i=1 新旧节点都是n2 patch打补丁更新

i=2 新节点n3!== n2 跳出循环

第二步:后置预处理

从后到前去遍历新旧索引列表

发现它们的节点相同,则直接patch

打更新

否则跳出循环

并记录e1 e2的值

vue3源码:

javascript 复制代码
        // 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--
    }

以下demo为例:

e1=6、e2=6对应都是n7节点,patch打补丁更新

e1=5、e2=5对应的新旧节点不同

跳出循环,记录e1、e2的值

第三步:处理仅有新增节点情况

当i > e1 并且i <= e2 :代表仅有新增节点

则直接patch打补丁更新

vue3源码:

javascript 复制代码
     // 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++
        }
      }
    }

以下demo为例:

i = 2时,新旧节点不同,从后往前遍历

e1=2、e2=6对应都是n7节点,patch打补丁更新

e1=1、e2=5对应的新旧节点不同

这时i > e1 并且i <= e2,仅有新增节点,直接更新

第四步:处理仅有卸载节点情况

当i > e2 并且i <= e1 :代表仅有卸载节点

则直接卸载节点

vue3源码:

javascript 复制代码
     // 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++
      }
    }

以下demo为例:

i = 2时,新旧节点不同,从后往前遍历

e1=2、e2=6对应都是n7节点,patch打补丁更新

e1=5、e2=1对应的新旧节点不同

这时i > e2 并且i <= e1 ,仅有卸载节点,直接卸载

第五步:处理其他情况(新增、卸载、移动)

定义以下变量

变量 定义
s1 记录旧节点列表要处理部分的起始位置
s2 记录新节点列表要处理部分的起始位置
keyToNewIndexMap 新节点位置映射表;用于映射新节点与位置的映射关系
newIndexToOldIndexMap 新旧节点位置映射表;用于记录新旧节点位置的映射关系。初始值为0,如果都是0,则判定为新节点,需要挂载
maxNewIndexSoFar 当前最远位置;用于记录新节点中当前的最远位置。目的是用于判断遍历过程中是否呈现递增趋势。如果不是则代表产生了移动
moved 递减趋势,需要移动则标识为true;后续进行移动处理
j 最长递增子序列位置

以下demo为列:

变动点:卸载n3 / 新增n8 / 移动n6

当s1=2 : n3 ;在新节点位置映射表内没有找到;则为卸载,把该节点移除

当s1=3: n4; 在新节点位置映射表中可以找到;则将s1 + 1 = 4放在新旧节点位置映射表中。同时对比新节点位置索引和最远位置,3 > 0,则将新索引位置记录到最远位置中。最后打补丁更新

当s1=4: n5; 在新节点位置映射表中可以找到;则将s1 + 1 = 5放在新旧节点位置映射表中。同时对比新节点位置索引和最远位置,4 > 3,则将新索引位置记录到最远位置中。最后打补丁更新

当s1=5: n6; 在新节点位置映射表中可以找到;则将s1 + 1 = 6放在新旧节点位置映射表中。同时对比新节点位置索引和最远位置,2 > 4,呈现递减趋势,说明位置发生了移动,则移动标识为true。最后patch打补丁更新

到这一步已经遍历完旧节点。这时候需要根据映射表找到最长递增子序列

目的是为了让节点做最小限度的移动操作

从下图中新旧节点映射表中可以看出:最长递增子序列索引值是4/5,将其记录下来,对应的就是1/2

从后开始往前遍历新旧节点映射表

定义变量 i 记录当前新旧节点映射表位置,

定义变量 j 记录最长递增子序列位置,初始化为1

当i=3:0 对应n8节点。0代表新增,挂载该节点

当i=2: 5 对应n5节点。j=1 对应最长递增子序列。因此无需移动,直接跳转

当i=1: 4 对应n4节点。j=2对应最长递增子序列中。因此无需移动,直接跳转

当i=0: 6对应n6节点。不处于最长递增子序列当中。因此需要移动,执行移动操作

这样下来,整个过程只需要挂载n8节点、卸载n3节点、移动n6节点,其他全部原地patch更新。性能得到了极大的保证

vue3源码:

javascript 复制代码
     // 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<PropertyKey, 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,
            namespace,
            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,
            namespace,
            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--
          }
        }
      }
    }

以上就是vue3 DOM DIFF算法的整个过程

vue3源码地址: https://github.com/vuejs/core

render文件:vue3\core\packages\runtime-core\src\renderer.ts

相关推荐
吾当每日三饮五升2 小时前
C++单例模式跨DLL调用问题梳理
开发语言·c++·单例模式
猫武士水星3 小时前
C++ scanf
开发语言·c++
BinaryBardC3 小时前
Bash语言的数据类型
开发语言·后端·golang
Lang_xi_3 小时前
Bash Shell的操作环境
linux·开发语言·bash
Pandaconda3 小时前
【Golang 面试题】每日 3 题(二十一)
开发语言·笔记·后端·面试·职场和发展·golang·go
捕鲸叉4 小时前
QT自定义工具条渐变背景颜色一例
开发语言·前端·c++·qt
想要入门的程序猿4 小时前
Qt菜单栏、工具栏、状态栏(右键)
开发语言·数据库·qt
Elena_Lucky_baby4 小时前
在Vue3项目中使用svg-sprite-loader
开发语言·前端·javascript
wjm0410064 小时前
贪心算法概述
算法·贪心算法
土豆凌凌七5 小时前
GO随想:GO的并发等待
开发语言·后端·golang