Vue Diff算法详解(附源码)

当我们在使用 Vue.js 开发应用时,Vue 内部会通过一种称为 “Virtual DOM” 的机制来进行 DOM 的更新,其中的关键部分是 “diff 算法”。Diff 算法的目标是在新旧 Virtual DOM 树之间找到最小的差异,以最小化对实际 DOM 的更新操作,提高性能。

什么是 虚拟DOM(Virtual DOM)?

Virtual DOM 是一个虚拟的浏览器内存中的 DOM 表示。它是由 JavaScript 对象构成的树形结构,模拟了实际的 DOM 结构。当你修改页面的时候,Vue 不会直接操作实际的 DOM,而是先操作 Virtual DOM,然后通过 “Diff 算法” 找出变化,最后才更新实际的 DOM。

简单理解一下虚拟Dom:

渲染页面的Dom操作等同于拼图游戏。实际Dom就是你的实体拼图,虚拟Dom则是拼图图纸,当你想要对拼图上(真实Dom)的某一块进行更新时,先在图纸上(虚拟Dom)标记出来需要变化的位置,然后再进行更新/替换,这样就只需要对拼图的一小部分进行修改(Diff算法),而不需要重新拼一整个拼图(重新渲染虚拟Dom)

为什么使用虚拟Dom

  • 性能优化: 直接修改实际 DOM 可能会触发浏览器的重新渲染,而虚拟 DOM 允许我们在内存中操作,最后一次性更新到实际 DOM,减少渲染次数。
  • 方便比对: 虚拟 DOM 是一个轻量级的 JavaScript 对象,易于比对。这就像你用图纸比对实际拼图的过程,找到差异并进行精准修改。

Diff 算法如何工作?

Diff 算法有三个步骤:分析变化、比较差异、更新 DOM。
分析变化: Vue 会对比新旧两棵 Virtual DOM 树,找出哪些地方发生了变化,哪些地方需要更新。
比较差异: 通过一些巧妙的算法,Diff 算法会找出最小的差异,包括节点的增加、删除、移动等。
更新 DOM: 最后,Vue 会根据 Diff 的结果,只更新发生变化的部分,而不是整个页面。

有key和无key的Diff算法

Diff算法的核心思想是

无keyDiff算法

在没有使用 key 的情况下,如果列表中的元素位置发生变化,Vue 会按照新旧列表的索引顺序进行比较,而不关心元素具体的内容

<!-- 无 Key 的情况 -->
<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

<!-- 更新后的 DOM -->
<ul>
  <li>D</li>
  <li>A</li>
  <li>C</li>
</ul>

  • A 在新列表中的位置由第一个变为第二个,因此 A 的位置发生了变化。
  • B 在新列表中已经不存在,所以需要删除。
  • C 在新列表中仍然存在,并且位置保持不变,不需要更新。

Diff算法 无key源码

两个 VNode 数组进行 Diff 操作的函数。这个函数会对比旧的 VNode 数组 c1 和新的 VNode 数组 c2,然后在容器 container 中进行相应的 DOM 操作。

  // 没有 key 的 diff 算法
const patchUnkeyedChildren = (
  c1: VNode[], // 旧的 VNode 数组
  c2: VNodeArrayChildren, // 新的 VNode 数组
  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 循环对比新旧 VNode
  for (i = 0; i < commonLength; i++) {
    // 对于优化过的情况,如果节点已挂载,则克隆节点
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    // 执行 patch 操作,更新或创建节点
    patch(
      c1[i],
      nextChild,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
  if (oldLength > newLength) {
    // 删除多余的旧节点
    unmountChildren(
      c1,
      parentComponent,
      parentSuspense,
      true,
      false,
      commonLength
    )
  } else {
    // 添加新的节点
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized,
      commonLength
    )
  }
}

核心工作是通过 for 循环遍历新旧 VNode 数组的共同部分,对每个对应位置的 VNode 进行 patch 操作。然后,根据新旧数组的长度差异,决定是删除多余的旧节点还是添加新的节点。

有key Diff算法

有 Key 的 Diff 算法同样在两个 VNode 数组进行比较,但它会借助 key 的信息更精准地更新节点。以下是这个算法的核心代码:


const patchKeyedChildren = (
  c1: VNode[], // 旧的 VNode 数组
  c2: VNodeArrayChildren, // 新的 VNode 数组
  container: RendererElement, // 容器
  parentAnchor: RendererNode | null, // 插入位置的锚点
  parentComponent: ComponentInternalInstance | null, // 父组件实例
  parentSuspense: SuspenseBoundary | null, // 父 Suspense 边界
  isSVG: boolean, // 是否为 SVG 元素
  slotScopeIds: string[] | null, // 插槽作用域的 ID 数组
  optimized: boolean // 是否优化
) => {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1 // 旧列表的结束索引
  let e2 = l2 - 1 // 新列表的结束索引

  // 1. 同步开始部分
  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 操作
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      // 如果不是相同类型的节点,则终止循环
      break
    }
    i++
  }

  // 2. 同步结束部分
  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 操作
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      // 如果不是相同类型的节点,则终止循环
      break
    }
    e1--
    e2--
  }

  // 3. 处理公共部分
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
      while (i <= e2) {
        // 在旧列表中找不到对应的节点,则执行 mount 操作
        patch(
          null,
          (c2[i] = optimized
            ? cloneIfMounted(c2[i] as VNode)
            : normalizeVNode(c2[i])),
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        i++
      }
    }
  }

  // 4. 处理未知序列 - 删除多余的旧节点
  else if (i > e2) {
    while (i <= e1) {
      // 删除多余的旧节点
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }

  // 5. 处理未知序列 - 处理新的节点
  else {
    const s1 = i // 旧列表的起始索引
    const s2 = i // 新列表的起始索引

    // 5.1 构建 key 到索引的映射关系
    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 遍历未知序列
    let j
    let patched = 0
    const toBePatched = e2 - s2 + 1
    let moved = false
    let maxNewIndexSoFar = 0
    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) {
        unmount(prevChild, parentComponent, parentSuspense, true)
        continue
      }
      let newIndex
      if (prevChild.key != null) {
        newIndex = keyToNewIndexMap.get(prevChild.key)
      } else {
        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 处理未知序列中的移动和添加
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : EMPTY_ARR
    j = increasingNewIndexSequence.length - 1
    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 操作
        patch(
          null,
          nextChild,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (moved) {
        // 如果发生了移动
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          // 如果不存在最长递增子序列或当前节点不在稳定序列中,执行移动操作
          move(nextChild, container, anchor, MoveType.REORDER)
        } else {
          // 在稳定序列中,继续查找
          j--
        }
      }
    }
  }
}

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue 3 使用了一种新的 diff 算法,称为 "优化后的 diff 算法"(Optimized Diff Algorithm),它与 Vue 2 中的 Virtual DOM diff 算法有一些不同之处。 在 Vue 2 中,Virtual DOM diff 算法会对整个虚拟 DOM 树进行深度遍历,并比较新旧两棵树的节点,找出差异并进行更新。这种算法在大型应用中可能会存在性能问题,因为它需要遍历整个树来找到差异。 而在 Vue 3 中,通过使用静态分析和标记技术,优化后的 diff 算法能够跳过不需要比较的子树,从而减少了比较的节点数量,提高了性能。具体来说,优化后的 diff 算法主要包含以下几个步骤: 1. 编译阶段:Vue 3 编译器会对模板进行静态分析,并为每个节点添加静态标记。静态节点是指在组件渲染过程中不会发生变化的节点。编译器还会为动态节点生成动态指令,在运行时根据需要进行处理。 2. 渲染阶段:在组件渲染阶段,Vue 3 会根据静态标记和动态指令进行渲染。对于静态节点,Vue 3 会跳过它们的比较,直接复用旧的节点。对于动态节点,Vue 3 会进行比较,并找出差异。 3. 差异更新:在找到差异后,Vue 3 会使用 patch 算法将差异应用到真实的 DOM 上。由于优化后的 diff 算法能够跳过不需要比较的子树,因此只有少量的节点需要进行更新,从而提高了性能。 总的来说,Vue 3 的优化后的 diff 算法通过静态分析和标记技术,能够跳过不需要比较的静态节点,从而减少了比较的节点数量,提高了性能。这是 Vue 3 在性能方面的一个重要改进。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值