Vue3 Diff算法之Element节点详解

183 阅读14分钟

已经完成了组件级别的渲染过程分析,组件最终会调用patch​函数,而 patch​ 函数会根据虚拟节点的类型进行不同处理。对于组件节点会递归处理其子组件,对于普通元素节点则会调用 processElement​ 函数进行处理。

这种递归调用形成了一个树形的渲染结构:从根组件开始,通过 patch​ 函数逐层向下处理,直到遇到普通元素节点。此时就从组件级别的渲染转换为 DOM 元素级别的渲染,通过 processElement​ 函数实现。

​​processElement函数解析

  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    if (n2.type === 'svg') {
      namespace = 'svg'
    } else if (n2.type === 'math') {
      namespace = 'mathml'
    }
    // 2. 根据是否存在旧节点选择挂载或更新
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    } else {
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    }
  }

mountElement详细分析

  const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const { props, shapeFlag, transition, dirs } = vnode

    el = vnode.el = hostCreateElement(
      vnode.type as string,
      namespace,
      props && props.is,
      props,
    )

    // mount children first, since some props may rely on child content
    // being already rendered, e.g. `<select value>`
   // 子节点处理分为两种情况:
	// 文本子节点:直接设置元素的文本内容,这是最简单的情况
    // 数组子节点:需要递归处理每个子节点,这可能会触发新的渲染周期
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(
        vnode.children as VNodeArrayChildren,
        el,
        null,
        parentComponent,
        parentSuspense,
        resolveChildrenNamespace(vnode, namespace),
        slotScopeIds,
        optimized,
      )
    }
	 // 3. 处理指令
    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'created')
    }
    // scopeId
    setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
   // 5. 处理props
    if (props) {
      for (const key in props) {
        if (key !== 'value' && !isReservedProp(key)) {
          hostPatchProp(el, key, null, props[key], namespace, parentComponent)
        }
      }
   
      if ('value' in props) {
        hostPatchProp(el, 'value', null, props.value, namespace)
      }
      if ((vnodeHook = props.onVnodeBeforeMount)) {
        invokeVNodeHook(vnodeHook, parentComponent, vnode)
      }
    }

    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
    }
    // #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
    // #1689 For inside suspense + suspense resolved case, just call it
    const needCallTransitionHooks = needTransition(parentSuspense, transition)
    if (needCallTransitionHooks) {
      transition!.beforeEnter(el)
    }
    hostInsert(el, container, anchor)
    if (
      (vnodeHook = props && props.onVnodeMounted) ||
      needCallTransitionHooks ||
      dirs
    ) {
      queuePostRenderEffect(() => {
        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
        needCallTransitionHooks && transition!.enter(el)
        dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
      }, parentSuspense)
    }
  }

patchElement详细分析

Vue提供了两种子节点更新策略:

  1. Block树更新(优化模式):

    • 只更新动态子节点
    • 跳过静态内容
    • 性能更好,适用于编译优化的情况
  2. 完整的子节点diff:

    • 对所有子节点进行比较
    • 使用传统的diff算法
    • 更通用,但性能较差
 const patchElement = (
  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
) => {
  // 1. 复用DOM元素
  const el = (n2.el = n1.el!)
  
  // 2. 获取新旧props和其他需要的信息
  let { patchFlag, dynamicChildren, dirs } = n2
  const oldProps = n1.props || EMPTY_OBJ
  const newProps = n2.props || EMPTY_OBJ

  // 3. 调用beforeUpdate钩子
  if (dirs) {
    invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
  }

  // 4. 处理子节点更新
  if (dynamicChildren) {
    // 优化模式:只更新动态子节点
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      resolveChildrenNamespace(n2, namespace),
      slotScopeIds,
    )
  } else if (!optimized) {
    // 完整diff
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      resolveChildrenNamespace(n2, namespace),
      slotScopeIds,
      false,
    )
  }

  // 5. 根据patchFlag优化更新props
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.FULL_PROPS) {
      // 完整props更新
      patchProps(el, oldProps, newProps, parentComponent, namespace)
    } else {
      // 按需更新class
      if (patchFlag & PatchFlags.CLASS) {
        if (oldProps.class !== newProps.class) {
          hostPatchProp(el, 'class', null, newProps.class, namespace)
        }
      }
      // 按需更新style
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, 'style', oldProps.style, newProps.style, namespace)
      }
      // 按需更新动态props
      if (patchFlag & PatchFlags.PROPS) {
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          if (next !== prev || key === 'value') {
            hostPatchProp(el, key, prev, next, namespace, parentComponent)
          }
        }
      }
    }
  }

  // 6. 调用updated钩子
  if ((dirs)) {
    queuePostRenderEffect(() => {
      dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
    }, parentSuspense)
  }
}

patchChildren函数分析

const patchBlockChildren = (
  oldChildren,
  newChildren,
  fallbackContainer,
  parentComponent,
  parentSuspense,
  namespace: ElementNamespace,
  slotScopeIds,
) => {
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i]
    const newVNode = newChildren[i]
    // 确定更新的容器
    const container =
      oldVNode.el &&
      (oldVNode.type === Fragment ||
        !isSameVNodeType(oldVNode, newVNode) ||
        oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
        ? hostParentNode(oldVNode.el)!
        : fallbackContainer
  
    // 对每个节点调用patch进行更新
    patch(
      oldVNode,
      newVNode,
      container,
      null,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      true,
    )
  }
}

核心设计

  1. 优化更新范围

    • 块树优化(Block Tree):

      1. 在编译阶段,Vue会将模板编译为渲染函数
      2. 编译器会标记出所有动态节点,收集到Block中
      3. 这些动态节点会形成一个扁平化的数组,称为"dynamicChildren"
      4. Block树中只有动态节点会被追踪,静态节点会被完全跳过
    • 动态节点收集:

      1. 编译器会识别模板中的动态绑定,如:

        • 动态属性:v-bind​、:​
        • 动态文本:{{ }}​
        • 动态指令:v-if​、v-for​等
      2. 这些动态节点会被赋予不同的 PatchFlag,用于标记其动态特性

      3. PatchFlag 会指示运行时如何更新这个节点

    • 更新优化:

      1. ​patchBlockChildren​ 只处理 dynamicChildren 数组中的节点
      2. 由于数组是扁平的,不需要递归遍历整个树结构
      3. 静态节点完全不会参与 diff 过程
      4. 动态节点可以直接一一对应更新,因为它们的顺序是稳定的
  2. 容器确定策略

    • fallbackContainer 是更新操作的默认容器,通常是当前正在处理的DOM元素

    • 在以下三种情况下,需要获取真实的父容器(hostParentNode)而不是使用 fallbackContainer:

      1. Fragment 类型:因为 Fragment 本身不会渲染成真实DOM,需要获取实际的父容器
      2. 新旧节点类型不同:需要在实际的父容器中完成替换操作
      3. 组件或传送门:这些特殊节点可能会改变DOM结构,需要确保在正确的容器中更新
    • 使用 fallbackContainer 的情况:

      1. 当节点类型相同且不是特殊节点时
      2. 这种情况下可以直接在当前容器中更新,无需获取父节点
      3. 这是一种优化手段,避免不必要的 DOM 父节点查找操作
  3. 更新方式

    • 直接调用patch
    • 保持节点顺序
    • 一对一更新,无需diff

patchChildren实现分析

const patchChildren = (
  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

  // 快速路径处理
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      // 处理带key的片段
      patchKeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
      return
    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      // 处理无key的片段
      patchUnkeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
      return
    }
  }

  // 处理三种可能的情况:文本、数组或无子节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 文本子节点的快速路径
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    if (c2 !== c1) {
      hostSetElementText(container, c2 as string)
    }
  } else {
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 之前的子节点是数组
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 两个数组,需要完整的diff
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      } else {
        // 没有新的子节点,卸载旧的
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
      }
    } else {
      // 之前的子节点是文本或null
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, '')
      }
      // 挂载新的数组子节点
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      }
    }
  }
}

更新策略分析

  1. PatchFlag优化

    • KEYED_FRAGMENT:带key的片段更新
    • UNKEYED_FRAGMENT:无key的片段更新
    • 利用编译时信息优化运行时性能
  2. 子节点类型处理

    • 文本子节点:直接替换
    • 数组子节点:需要diff算法
    • 无子节点:直接清空
  3. 不同场景的优化

    • 数组 -> 数组:完整diff
    • 数组 -> 文本:卸载后设置文本
    • 文本 -> 数组:清空后挂载
    • 文本 -> 文本:直接替换

总结

通过分析这两个函数,我们可以看到Vue在DOM更新时采用了多层次的优化策略:

  1. Block树优化

    • 编译时收集动态节点到 dynamicChildren 数组
    • 扁平化的动态节点数组,避免树形递归
    • 静态节点完全跳过,不参与更新过程
  2. 更新类型优化

    • 基于 PatchFlag 的快速路径处理
    • 针对性处理 KEYED_FRAGMENT 和 UNKEYED_FRAGMENT
    • 区分文本节点和数组节点的更新策略
  3. DOM操作优化

    • 复用 DOM 节点,避免不必要的创建和销毁
    • 优化容器查找策略,减少 DOM 父节点查找
    • 根据节点类型选择最优的更新路径

这些优化策略让Vue能够在保证功能的同时,最小化DOM操作次数,提供高效的更新性能。在下一篇文章中,我们将深入分析 patchKeyedChildren​ 函数和patchUnkeyedChildren​函数,了解Vue的核心diff算法实现。

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) {
    // 移除多余的旧节点
    unmountChildren(
      c1,
      parentComponent,
      parentSuspense,
      true,
      false,
      commonLength,
    )
  } else {
    // 挂载新增的节点
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized,
      commonLength,
    )
  }
}

patchKeyedChildren实现分析

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 // 旧节点的结束索引
  let e2 = l2 - 1       // 新节点的结束索引

  // 1. 从头部开始同步
  // (a b) c
  // (a b) d e
  // 这里的 a b 是相同的前缀节点,可以直接复用
  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++
  }

  // 2. 从尾部开始同步
  // a (b c)
  // d e (b c)
  // 这里的 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--
  }

  // 3. 处理新增节点
  // (a b)
  // (a b) c
  // i = 2, e1 = 1, e2 = 2
  // 这种情况下需要新增节点 c
  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], container, anchor, parentComponent,
          parentSuspense, namespace, slotScopeIds, optimized)
        i++
      }
    }
  }

  // 4. 处理需要删除的节点
  // (a b) c
  // (a b)
  // i = 2, e1 = 2, e2 = 1
  // 这种情况下需要删除节点 c
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }

  // 5. 处理未知序列
  // 例如,有如下更新:
  // 旧子节点: [p q] [a b c d e] [x y]
  // 新子节点: [p q] [c e b a d f] [x y]
  // 经过前面的首尾处理后:
  // - 先同步了前缀 [p q]
  // - 再同步了后缀 [x y]
  // - 剩余中间部分 [a b c d e] -> [c e b a d f] 需要处理
  else {
    const s1 = i
    const s2 = i

    // 5.1 建立新节点的key到索引的映射
    // 为剩余的 [c e b a d f] 建立映射:
    // {
    //   c: 0,
    //   e: 1,
    //   b: 2,
    //   a: 3,
    //   d: 4,
    //   f: 5
    // }
    const keyToNewIndexMap = 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) {
        keyToNewIndexMap.set(nextChild.key, i)
      }
    }

    // 5.2 遍历旧节点,尝试patch匹配的节点并移除不再存在的节点
    // 遍历剩余的旧子节点 [a b c d e],尝试在新子节点中找到对应位置
    // newIndexToOldIndexMap 记录新子节点在旧子节点中的位置
    // 对于 [a b c d e] -> [c e b a d f] 的映射关系是:
    // newIndexToOldIndexMap: [3, 5, 2, 1, 4, 0]
    // 这个数组表示:
    // 索引: 0  1  2  3  4  5
    // 新节点: c  e  b  a  d  f
    // 旧位置: 3  5  2  1  4  0  (注:实际值都+1了,0表示新节点)
    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) {
        // 所有新节点都已patch,剩余的旧节点可以直接删除
        unmount(prevChild, parentComponent, parentSuspense, true)
        continue
      }

      let newIndex
      if (prevChild.key != null) {
        // 通过key快速找到新的位置
        newIndex = keyToNewIndexMap.get(prevChild.key)
      } else {
        // 没有key只能遍历查找
        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 移动和挂载
    // 最长递增子序列是 [0, 4],对应新节点序列中的c和d
    // 这意味着这两个节点可以保持相对位置不变,其他节点需要移动
    //
    // Vue从后往前遍历处理节点:
    // 1. f是新节点,直接创建并插入
    // 2. d在最长递增子序列中,保持不动
    // 3. a需要移动到d之前
    // 4. b需要移动到a之前
    // 5. e需要移动到b之前
    // 6. c在最长递增子序列中,保持不动
    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
      // 确定锚点(下一个节点的DOM元素)
      const anchor =
        nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
    
      if (newIndexToOldIndexMap[i] === 0) {
        // 新节点,需要挂载
        // 例如例子中的节点f
        patch(null, nextChild, container, anchor, parentComponent,
          parentSuspense, namespace, slotScopeIds, optimized)
      } else if (moved) {
        // 需要移动的节点
        // 如果没有最长递增子序列,或当前节点不在最长递增子序列中
        // 例如例子中的节点e、b、a需要移动
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor, MoveType.REORDER)
        } else {
          // 当前节点在最长递增子序列中,不需要移动
          // 例如例子中的节点c和d
          j--
        }
      }
    }
  }
}

diff过程采用的方法


  const move: MoveFn = (
    vnode,
    container,
    anchor,
    moveType,
    parentSuspense = null,
  ) => {
    const { el, type, transition, children, shapeFlag } = vnode
    if (shapeFlag & ShapeFlags.COMPONENT) {
      move(vnode.component!.subTree, container, anchor, moveType)
      return
    }

    if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
      vnode.suspense!.move(container, anchor, moveType)
      return
    }

    if (shapeFlag & ShapeFlags.TELEPORT) {
      ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals)
      return
    }

    if (type === Fragment) {
      hostInsert(el!, container, anchor)
      for (let i = 0; i < (children as VNode[]).length; i++) {
        move((children as VNode[])[i], container, anchor, moveType)
      }
      hostInsert(vnode.anchor!, container, anchor)
      return
    }

    if (type === Static) {
      moveStaticNode(vnode, container, anchor)
      return
    }

    // single nodes
    const needTransition =
      moveType !== MoveType.REORDER &&
      shapeFlag & ShapeFlags.ELEMENT &&
      transition
    if (needTransition) {
      if (moveType === MoveType.ENTER) {
        transition!.beforeEnter(el!)
        hostInsert(el!, container, anchor)
        queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
      } else {
        const { leave, delayLeave, afterLeave } = transition!
        const remove = () => hostInsert(el!, container, anchor)
        const performLeave = () => {
          leave(el!, () => {
            remove()
            afterLeave && afterLeave()
          })
        }
        if (delayLeave) {
          delayLeave(el!, remove, performLeave)
        } else {
          performLeave()
        }
      }
    } else {
      hostInsert(el!, container, anchor)
    }
  }

  const unmount: UnmountFn = (
    vnode,
    parentComponent,
    parentSuspense,
    doRemove = false,
    optimized = false,
  ) => {
    const {
      type,
      props,
      ref,
      children,
      dynamicChildren,
      shapeFlag,
      patchFlag,
      dirs,
      cacheIndex,
    } = vnode

    if (patchFlag === PatchFlags.BAIL) {
      optimized = false
    }

    // unset ref
    if (ref != null) {
      setRef(ref, null, parentSuspense, vnode, true)
    }

    // #6593 should clean memo cache when unmount
    if (cacheIndex != null) {
      parentComponent!.renderCache[cacheIndex] = undefined
    }

    if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
      ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
      return
    }

    const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
    const shouldInvokeVnodeHook = !isAsyncWrapper(vnode)

    let vnodeHook: VNodeHook | undefined | null
    if (
      shouldInvokeVnodeHook &&
      (vnodeHook = props && props.onVnodeBeforeUnmount)
    ) {
      invokeVNodeHook(vnodeHook, parentComponent, vnode)
    }

    if (shapeFlag & ShapeFlags.COMPONENT) {
      unmountComponent(vnode.component!, parentSuspense, doRemove)
    } else {
      if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        vnode.suspense!.unmount(parentSuspense, doRemove)
        return
      }

      if (shouldInvokeDirs) {
        invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
      }

      if (shapeFlag & ShapeFlags.TELEPORT) {
        ;(vnode.type as typeof TeleportImpl).remove(
          vnode,
          parentComponent,
          parentSuspense,
          internals,
          doRemove,
        )
      } else if (
        dynamicChildren &&
        // #5154
        // when v-once is used inside a block, setBlockTracking(-1) marks the
        // parent block with hasOnce: true
        // so that it doesn't take the fast path during unmount - otherwise
        // components nested in v-once are never unmounted.
        !dynamicChildren.hasOnce &&
        // #1153: fast path should not be taken for non-stable (v-for) fragments
        (type !== Fragment ||
          (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
      ) {
        // fast path for block nodes: only need to unmount dynamic children.
        unmountChildren(
          dynamicChildren,
          parentComponent,
          parentSuspense,
          false,
          true,
        )
      } else if (
        (type === Fragment &&
          patchFlag &
            (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) ||
        (!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
      ) {
        unmountChildren(children as VNode[], parentComponent, parentSuspense)
      }

      if (doRemove) {
        remove(vnode)
      }
    }

    if (
      (shouldInvokeVnodeHook &&
        (vnodeHook = props && props.onVnodeUnmounted)) ||
      shouldInvokeDirs
    ) {
      queuePostRenderEffect(() => {
        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
        shouldInvokeDirs &&
          invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
      }, parentSuspense)
    }
  }

  const remove: RemoveFn = vnode => {
    const { type, el, anchor, transition } = vnode
    if (type === Fragment) {
      removeFragment(el!, anchor!)
      return
    }

    if (type === Static) {
      removeStaticNode(vnode)
      return
    }

    const performRemove = () => {
      hostRemove(el!)
      if (transition && !transition.persisted && transition.afterLeave) {
        transition.afterLeave()
      }
    }

    if (
      vnode.shapeFlag & ShapeFlags.ELEMENT &&
      transition &&
      !transition.persisted
    ) {
      const { leave, delayLeave } = transition
      const performLeave = () => leave(el!, performRemove)
      if (delayLeave) {
        delayLeave(vnode.el!, performRemove, performLeave)
      } else {
        performLeave()
      }
    } else {
      performRemove()
    }
  }

  const removeFragment = (cur: RendererNode, end: RendererNode) => {
    // For fragments, directly remove all contained DOM nodes.
    // (fragment child nodes cannot have transition)
    let next
    while (cur !== end) {
      next = hostNextSibling(cur)!
      hostRemove(cur)
      cur = next
    }
    hostRemove(end)
  }

  const unmountComponent = (
    instance: ComponentInternalInstance,
    parentSuspense: SuspenseBoundary | null,
    doRemove?: boolean,
  ) => {
    const { bum, scope, update, subTree, um, m, a } = instance
    invalidateMount(m)
    invalidateMount(a)

    // beforeUnmount hook
    if (bum) {
      invokeArrayFns(bum)
    }

    if (
      __COMPAT__ &&
      isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
    ) {
      instance.emit('hook:beforeDestroy')
    }

    // stop effects in component scope
    scope.stop()

    // update may be null if a component is unmounted before its async
    // setup has resolved.
    if (update) {
      // so that scheduler will no longer invoke it
      update.active = false
      unmount(subTree, instance, parentSuspense, doRemove)
    }
    // unmounted hook
    if (um) {
      queuePostRenderEffect(um, parentSuspense)
    }
    if (
      __COMPAT__ &&
      isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
    ) {
      queuePostRenderEffect(
        () => instance.emit('hook:destroyed'),
        parentSuspense,
      )
    }
    queuePostRenderEffect(() => {
      instance.isUnmounted = true
    }, parentSuspense)

    // A component with async dep inside a pending suspense is unmounted before
    // its async dep resolves. This should remove the dep from the suspense, and
    // cause the suspense to resolve immediately if that was the last dep.
    if (
      __FEATURE_SUSPENSE__ &&
      parentSuspense &&
      parentSuspense.pendingBranch &&
      !parentSuspense.isUnmounted &&
      instance.asyncDep &&
      !instance.asyncResolved &&
      instance.suspenseId === parentSuspense.pendingId
    ) {
      parentSuspense.deps--
      if (parentSuspense.deps === 0) {
        parentSuspense.resolve()
      }
    }
  }

  const unmountChildren: UnmountChildrenFn = (
    children,
    parentComponent,
    parentSuspense,
    doRemove = false,
    optimized = false,
    start = 0,
  ) => {
    for (let i = start; i < children.length; i++) {
      unmount(children[i], parentComponent, parentSuspense, doRemove, optimized)
    }
  }

总结

通过分析这两个函数,我们可以看到Vue在处理子节点更新时采用了不同的策略:

  1. 无key子节点

    • 简单的位置对应更新
    • 适用于静态或很少变化的列表
    • 性能较差,不建议在动态列表中使用
  2. 带key子节点

    • 复杂但高效的diff算法
    • 优化的节点复用策略
    • 最小化DOM操作
  3. 性能优化

    • 多级别的优化策略
    • 智能的节点复用
    • 最优的DOM操作序列

这种分层的更新策略让Vue能够在不同场景下都能提供最优的更新性能。在实际开发中,我们应该:

  1. 总是为动态列表提供key
  2. 避免使用索引作为key
  3. 优先考虑稳定且唯一的值作为key

参考:juejin.cn/post/745867…