Vue3 Diff算法之基础节点详解

119 阅读6分钟

mount函数创建虚拟节点之后就会调用render函数,而render函数内部的核心就是patch函数,也就是大名鼎鼎的diff算法。

 const patch: PatchFn = (
    n1, // 旧的虚拟节点
    n2, // 新的虚拟节点
    container, // 容器元素
    anchor = null, // 锚点元素
    parentComponent = null, // 父组件实例
    parentSuspense = null, // 父 Suspense 组件
    namespace = undefined, // 命名空间
    slotScopeIds = null, // 插槽作用域 ID
    optimized = !!n2.dynamicChildren, // 是否优化
  ) => {
    if (n1 === n2) {
      return
    }

    // 1. 处理新旧节点类型不同的情况
    if (n1 && !isSameVNodeType(n1, n2)) {
      // 1. 获取下一个节点作为锚点
      anchor = getNextHostNode(n1)
      // 2. 卸载旧节点
      unmount(n1, parentComponent, parentSuspense, true)
      // 3. 重置旧节点引用
      n1 = null
    }

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

    // 2. 根据新节点的类型选择不同的处理方式
    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        // 处理文本节点
        processText(n1, n2, container, anchor)
        break
      case Comment:
        // 处理注释节点
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        // 处理静态节点
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, namespace)
        }
        break
      case Fragment:
        // 处理片段节点
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        break
      default:
        // 3. 处理组件或元素节点
        if (shapeFlag & ShapeFlags.ELEMENT) {
          // 处理普通元素
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // 处理组件
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          //  处理TELEPORT
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
            internals,
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          // 处理SUSPENSE
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
            internals,
          )
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

Vue 中的节点类型可以分为以下几类:

  1. 基础节点类型

    • Text: 文本节点
    • Comment: 注释节点
    • Static: 静态节点
    • Fragment: 片段节点
  2. 复杂节点类型

    • Element: 普通元素节点
    • Component: 组件节点
    • Teleport: 传送门组件
    • Suspense: 异步组件
Text 节点处理

源码实现

 const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      // 首次挂载:创建文本节点并插入
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor,
      )
    } else {
      // 更新:复用旧节点,只更新文本内容
      const el = (n2.el = n1.el!)
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string)
      }
    }
  }

使用示例

<!-- 直接文本 -->
Hello World

<!-- 动态文本 -->
{{ message }}
Comment 节点处理

源码实现

const processCommentNode: ProcessTextOrCommentFn = (
    n1,
    n2,
    container,
    anchor,
  ) => {
    if (n1 == null) {
      // 首次挂载:创建并插入注释节点
      hostInsert(
        (n2.el = hostCreateComment((n2.children as string) || '')),
        container,
        anchor,
      )
    } else {
      // 更新:注释节点不支持动态更新,直接复用
      n2.el = n1.el
    }
  }

使用示例

<!-- 这是一个注释 -->
<!-- 
  多行注释
  用于调试或文档说明
-->
Static节点处理

源码实现

// 挂载静态节点  
const mountStaticNode = (
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    namespace: ElementNamespace,
  ) => {
    // static nodes are only present when used with compiler-dom/runtime-dom
	// 静态节点仅在使用 compiler-dom/runtime-dom 时存在
    ;[n2.el, n2.anchor] = hostInsertStaticContent!(
      n2.children as string,
      container,
      anchor,
      namespace,
      n2.el,
      n2.anchor,
    )
  }
// 移动静态节点
const moveStaticNode = (
    { el, anchor }: VNode,
    container: RendererElement,
    nextSibling: RendererNode | null,
  ) => {
    let next
    while (el && el !== anchor) {
      next = hostNextSibling(el)
      hostInsert(el, container, nextSibling)
      el = next
    }
    hostInsert(anchor!, container, nextSibling)
  }
	// 移除静态节点
  const removeStaticNode = ({ el, anchor }: VNode) => {
    let next
    while (el && el !== anchor) {
      next = hostNextSibling(el)
      hostRemove(el)
      el = next
    }
    hostRemove(anchor!)
  }

使用示例

<!-- 编译时优化的静态内容 -->
<div class="static">
  <h1>静态标题</h1>
  <p>静态段落</p>
</div>

<!-- 带有静态树的模板 -->
<template>
  <div>
    <header>
      <!-- 这部分在编译时被标记为静态 -->
      <logo />
      <nav>
        <a href="/">首页</a>
        <a href="/about">关于</a>
      </nav>
    </header>
    <!-- 动态内容 -->
    <main>{{ content }}</main>
  </div>
</template>
Fragment 节点处理
  const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    // 创建或复用首尾锚点
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

    let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2

    // check if this is a slot fragment with :slotted scope ids
    if (fragmentSlotScopeIds) {
      slotScopeIds = slotScopeIds
        ? slotScopeIds.concat(fragmentSlotScopeIds)
        : fragmentSlotScopeIds
    }

    if (n1 == null) {
      // 首次挂载:插入锚点并挂载子节点
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // a fragment can only have array children
      // since they are either generated by the compiler, or implicitly created
      // from arrays.
      mountChildren(
        // #10007
        // such fragment like `<></>` will be compiled into
        // a fragment which doesn't have a children.
        // In this case fallback to an empty array
        (n2.children || []) as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    } else {
      // 更新:根据情况选择更新策略
      if (
        patchFlag > 0 &&
        patchFlag & PatchFlags.STABLE_FRAGMENT &&
        dynamicChildren &&
        // #2715 the previous fragment could've been a BAILed one as a result
        // of renderSlot() with no valid children
        n1.dynamicChildren
      ) {
        // 更新动态块
        patchBlockChildren(
          n1.dynamicChildren,
          dynamicChildren,
          container,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
        )
        if (
          // #2080 if the stable fragment has a key, it's a <template v-for> that may
          //  get moved around. Make sure all root level vnodes inherit el.
          // #2134 or if it's a component root, it may also get moved around
          // as the component is being moved.
          n2.key != null ||
          (parentComponent && n2 === parentComponent.subTree)
        ) {
          traverseStaticChildren(n1, n2, true /* shallow */)
        }
      } else {
        // 全量更新
        patchChildren(
          n1,
          n2,
          container,
          fragmentEndAnchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      }
    }
  }

使用示例

<!-- 多根节点模板 -->
<template>
  <div>First</div>
  <div>Second</div>
</template>

<!-- v-for 片段 -->
<template>
  <div v-for="item in items">{{ item }}</div>
</template>

<!-- 结构固定,仅内容变化 -->
<template>
  <div>固定的</div>
  <div>{{ dynamic }}</div>
</template>
Element 节点处理

源码实现

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
) => {
  // 1. 处理命名空间
  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
    );
  }
};
Component 节点

源码实现

const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  // 1. 继承slot作用域id
   n2.slotScopeIds = slotScopeIds

  if (n1 == null) {
    // 2. 挂载组件
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      // keepAlive 激活
      (parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        namespace,
        optimized
      );
    } else {
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized
      );
    }
  } else {
    // 3. 更新组件
    updateComponent(n1, n2, optimized);
  }
}
Teleport 节点

源码实现

const processTeleport = (
  n1: TeleportVNode | null,
  n2: TeleportVNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
  internals: RendererInternals
) => {
  // 1. 获取目标容器
  const targetSelector = n2.props && n2.props.to;
  const target =
    typeof targetSelector === "string"
      ? document.querySelector(targetSelector)
      : targetSelector;

  // 2. 处理传送
  if (n1 == null) {
    // 首次挂载
    mountChildren(
      n2.children as VNodeArrayChildren,
      target || container,
      null,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized
    );
  } else {
    // 更新处理
    patchChildren(
      n1,
      n2,
      target || container,
      null,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized
    );
  }
};
Suspense 节点

源码实现

const processSuspense = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
  rendererInternals: RendererInternals
) => {
  if (n1 == null) {
    // 1. 首次挂载
    mountSuspense(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized,
      rendererInternals
    );
  } else {
    // 2. 更新处理
    patchSuspense(
      n1,
      n2,
      container,
      anchor,
      parentComponent,
      slotScopeIds,
      optimized,
      rendererInternals
    );
  }
};

使用示例

<Suspense>
  <!-- 异步组件 -->
  <template #default>
    <AsyncComponent />
  </template>

  <!-- 加载状态 -->
  <template #fallback>
    <LoadingComponent />
  </template>
</Suspense>

处理策略比较(摘录)

1. 更新策略

节点类型首次挂载更新处理优化机制特殊处理
Text创建文本节点直接更新内容
Comment创建注释节点不支持更新开发环境保留
Static一次性挂载开发环境才更新编译时优化使用锚点标记范围
Fragment创建锚点可能优化更新blocktree优化处理多根节点
Element创建 DOM 元素属性和子节点 diffblocktree优化命名空间处理
Component创建组件实例状态驱动更新Props 优化生命周期管理
Teleport创建传送内容目标更新处理复用 DOM跨层级渲染
Suspense创建异步边界状态切换更新内容缓存嵌套处理