Vue3 源码解读之patch算法(一)

402 阅读22分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情 >>

版本:3.2.31

Vue 在处理虚拟DOM的更新时,会对新旧两个 VNode 节点通过 Diff 算法进行比较,然后通过对比结果找出差异的节点或属性进行按需更新。这个 Diff 过程,在 Vue 中叫作 patch 过程,patch 的过程就是以新的 VNode 为基准,去更新旧的 VNode。

接下来,我们通过源码来看看 patch 过程中做了哪些事情。

patch 函数

// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
  n1, // 旧虚拟节点
  n2, // 新虚拟节点
  container,
  anchor = null, // 定位锚点DOM,用于往锚点前插入节点
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // 是否启用 diff 优化
) => {
  // 新旧虚拟节点相同,直接返回,不做 Diff 比较
  if (n1 === n2) {
    return
  }

  // patching & not same type, unmount old tree
  // 新旧虚拟节点不相同(key 和 type 不同),则卸载旧的虚拟节点及其子节点
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    // 卸载旧的虚拟节点及其子节点
    unmount(n1, parentComponent, parentSuspense, true)
    // 将 旧虚拟节点置为 null,保证后面走整个节点的 mount 逻辑
    n1 = null
  }

  // PatchFlags.BAIL 标志用于指示应该退出 diff 优化
  if (n2.patchFlag === PatchFlags.BAIL) {
    // optimized 置为 false ,在后续的 Diff 过程中不会启用 diff 优化
    optimized = false
    // 将新虚拟节点的动态子节点数组置为 null,则不会进行 diff 优化
    n2.dynamicChildren = null
  }

  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, isSVG)
      } else if (__DEV__) {
        // 更新静态节点
        patchStaticNode(n1, n2, container, isSVG)
      }
      break
    case Fragment: // 处理 Fragment 元素
      processFragment(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 处理 ELEMENT 类型的 DOM 元素
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 处理组件
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        // 处理 Teleport 组件
        // 调用 Teleport 组件内部的 process 函数,渲染 Teleport 组件的内容
        ;(type as typeof TeleportImpl).process(
          n1 as TeleportVNode,
          n2 as TeleportVNode,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized,
          internals
        )
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        // 处理 Suspense 组件
        // 调用 Suspense 组件内部的 process 函数,渲染 Suspense 组件的内容
        ;(type as typeof SuspenseImpl).process(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized,
          internals
        )
      } else if (__DEV__) {
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }

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

从上面的源码中,我们可以清晰地看到 patch 过程所做的事情:

  1. 如果新旧虚拟节点相同 (n1 === n2),则直接返回,不做 Diff 比较。
  2. 如果新旧虚拟节点不相同,则直接卸载旧的虚拟节点及其子节点。同时将旧虚拟节点n1 置为 null,这样就保证了新节点可以正常挂载。
  3. 判断新虚拟节点的 patchFlag 类型是否为 PatchFlags.BAIL,则将 optimized 置为 false,那么在后续的 Diff 过程中就不会启用 diff 优化。同时也将新虚拟节点的动态子节点数组 dynamicChildren 置为 null,在后续 Diff 过程中也不会启用 diff 优化。
  4. 然后根据新虚拟节点的 type 类型,分别对文本节点、注释节点、静态节点以及Fragment节点调用相应的处理函数对其进行处理。
  5. 接着根据 shapeFlag 的类型,调用不同的处理函数,分别对 Element类型的节点、Component 组件、Teleport 组件、Suspense 异步组件进行处理。
  6. 最后,调用了 setRef 函数来设置 ref 引用。

processText 处理文本节点

// packages/runtime-core/src/renderer.ts

// 处理文本
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  if (n1 == null) {
    // 首次挂载时,新建一个文本节点到 container 中
    // 并将文本元素存储到新虚拟节点的 el 属性上
    hostInsert(
      (n2.el = hostCreateText(n2.children as string)),
      container,
      anchor
    )
  } else {
    // 获取旧虚拟节点的真实 DOM 元素
    // 同时将新虚拟节点的 el 指向旧虚拟节点指向的真实 DOM 元素
    const el = (n2.el = n1.el!)
    // .children 就是文本内容
    // 新旧节点的文本内容不同,则将真实 DOM 元素的文本内容更新为新的文本内容
    if (n2.children !== n1.children) {
      hostSetText(el, n2.children as string)
    }
  }
}

在处理文本节点时,如果 n1 为 null,即在首次挂载时调用 hostInsert 方法,即调用原生DOM API insertBefore 方法,将新建的文本元素插入到 container 中,同时将文本元素存储到新虚拟节点 n2 的 el 属性上,保持对真实DOM元素的引用。

如果 n1 不为null,说明是在更新阶段,此时判断新旧节点的文本内容是否相同,如果不同,则调用 hostSetText 方法将真实 DOM 元素的文本内容更新为新的文本内容。

processCommentNode 处理注释节点

// packages/runtime-core/src/renderer.ts

// 处理注释
const processCommentNode: ProcessTextOrCommentFn = (
  n1,
  n2,
  container,
  anchor
) => {
  if (n1 == null) {
    // 首次挂载时,新建一个注释节点到 container 中
    // 并将注释节点存储到新虚拟节点的 el 属性上
    hostInsert(
      (n2.el = hostCreateComment((n2.children as string) || '')),
      container,
      anchor
    )
  } else {
    // there's no support for dynamic comments
    // 将新虚拟节点的 el 置为旧虚拟节点的 el
    n2.el = n1.el
  }
}

可以看到,处理注释节点的思路和处理文本节点的思路相似。在首次挂载时调用 hostInsert 方法,将新建的注释插入到 container 中,同时将文本元素存储到新虚拟节点 n2 的 el 属性上,保持对真实DOM元素的引用。在更新阶段,则是直接将新虚拟节点的 el 设置为旧虚拟节点的 el。

mountStaticNode/patchStaticNode 处理静态节点

// packages/runtime-core/src/renderer.ts

// 挂载静态节点
const mountStaticNode = (
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  isSVG: boolean
) => {
  // static nodes are only present when used with compiler-dom/runtime-dom
  // which guarantees presence of hostInsertStaticContent.
  // 向 container 中插入一个静态节点
  ;[n2.el, n2.anchor] = hostInsertStaticContent!(
    n2.children as string,
    container,
    anchor,
    isSVG,
    n2.el,
    n2.anchor
  )
}

/**
 * Dev / HMR only
 */
// 仅用于开发环境 热更新
const patchStaticNode = (
  n1: VNode,
  n2: VNode,
  container: RendererElement,
  isSVG: boolean
) => {
  // static nodes are only patched during dev for HMR
  if (n2.children !== n1.children) {
    const anchor = hostNextSibling(n1.anchor!)
    // remove existing
    // 移除已经存在的静态节点
    removeStaticNode(n1)
    // insert new
    // 插入一个新的静态节点
    ;[n2.el, n2.anchor] = hostInsertStaticContent!(
      n2.children as string,
      container,
      anchor,
      isSVG
    )
  } else {
    // 新虚拟节点的 el 指向 旧虚拟节点的 el
    n2.el = n1.el
    // 新虚拟节点的 anchor 指向 旧虚拟节点的 anchor
    n2.anchor = n1.anchor
  }
}

可以看到,挂载静态节点时,调用了hostInsertStaticContent 函数向 container 中插入一个静态节点。对于静态节点的更新,只在开发环境时做更新处理。在做更新处理时,先移除已经存在的静态节点,然后再往 container 中插入一个新的静态节点。

processFragment 处理 Fragment 元素

// packages/runtime-core/src/renderer.ts

const processFragment = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  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

  if (__DEV__ && isHmrUpdating) {
    // HMR updated, force full diff
    patchFlag = 0
    optimized = false
    dynamicChildren = null
  }

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

  if (n1 == null) {
    // 首次挂载时插入 Fragment
    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(
      n2.children as VNodeArrayChildren,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      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
    ) {
      // a stable fragment (template root or <template v-for>) doesn't need to
      // patch children order, but it may contain dynamicChildren.
      // 稳定的 Fragment (例如:template root or <template v-for>) 不需要更新整个 block
      // 但是可能还会包含动态子节点,因此需要对动态子节点进行更新
      patchBlockChildren(
        n1.dynamicChildren,
        dynamicChildren,
        container,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds
      )
      if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
        traverseStaticChildren(n1, n2)
      } else 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 {
      // keyed / unkeyed, or manual fragments.
      // for keyed & unkeyed, since they are compiler generated from v-for,
      // each child is guaranteed to be a block so the fragment will never
      // have dynamicChildren.

      // 更新子节点
      patchChildren(
        n1,
        n2,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }
}

在首次渲染 DOM 树时,创建 FragmentStart 和 FragmentEnd,并将它们插入到 container 中,然后调用 mountChildren 挂载 Fragment 的所有子节点。

在更新阶段,Fragment 是稳定的,并且存在动态子节点,则调用 patchBlockChildren 函数对子节点进行更新,否则直接调用 patchChildren 函数更新子节点。

processElement 处理 Element

// packages/runtime-core/src/renderer.ts

// 处理 ELEMENT 类型的 DOM 元素
const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  isSVG = isSVG || (n2.type as string) === 'svg'
  if (n1 == null) {
    // 挂载 Element 节点
    mountElement(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    // 更新 Element 节点
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

从源码中可以看到,在首次渲染 DOM 树时,调用 mountElement 函数挂载 Element 节点。在更新阶段,则调用 patchElement 函数来更新 Element 节点。

mountElement 挂载 Element 节点

// packages/runtime-core/src/renderer.ts

const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  let el: RendererElement
  let vnodeHook: VNodeHook | undefined | null
  const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
  if (
    !__DEV__ &&
    vnode.el &&
    hostCloneNode !== undefined &&
    patchFlag === PatchFlags.HOISTED
  ) {
    // If a vnode has non-null el, it means it's being reused.
    // Only static vnodes can be reused, so its mounted DOM nodes should be
    // exactly the same, and we can simply do a clone here.
    // only do this in production since cloned trees cannot be HMR updated.

    // 复用静态节点
    el = vnode.el = hostCloneNode(vnode.el)
  } else {
    //  创建 DOM 节点
    el = vnode.el = hostCreateElement(
      vnode.type as string,
      isSVG,
      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) {
      // 设置 DOM节点的文本内容
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 挂载子节点
      mountChildren(
        vnode.children as VNodeArrayChildren,
        el,
        null,
        parentComponent,
        parentSuspense,
        isSVG && type !== 'foreignObject',
        slotScopeIds,
        optimized
      )
    }

    // 处理指令
    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'created')
    }
    // props
    // 处理 DOM 节点上的 props 
    if (props) {
      for (const key in props) {
        if (key !== 'value' && !isReservedProp(key)) {
          // 对 props 进行 diff 
          hostPatchProp(
            el,
            key,
            null,
            props[key],
            isSVG,
            vnode.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
      /**
       * Special case for setting value on DOM elements:
       * - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
       * - it needs to be forced (#1471)
       * #2353 proposes adding another renderer option to configure this, but
       * the properties affects are so finite it is worth special casing it
       * here to reduce the complexity. (Special casing it also should not
       * affect non-DOM renderers)
       */
      if ('value' in props) {
        // 对DOM节点上的 value 属性进行diff,如<select value>
        hostPatchProp(el, 'value', null, props.value)
      }
      if ((vnodeHook = props.onVnodeBeforeMount)) {
        invokeVNodeHook(vnodeHook, parentComponent, vnode)
      }
    }
    // scopeId
    // 设置 DOM 的一些 attr 属性
    setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
  }
  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    Object.defineProperty(el, '__vnode', {
      value: vnode,
      enumerable: false
    })
    Object.defineProperty(el, '__vueParentComponent', {
      value: parentComponent,
      enumerable: false
    })
  }
  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
  // 执行动画相关的生命周期钩子
  // 判断一个 VNode 是否需要过渡
  const needCallTransitionHooks =
    (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
    transition &&
    !transition.persisted
  if (needCallTransitionHooks) {
    // 在挂载DOM元素之前执行动画的 beforeEnter 生命周期钩子函数
    transition!.beforeEnter(el)
  }
  // 挂载DOM元素
  hostInsert(el, container, anchor)

  // 挂载完DOM元素后,执行动画的 enter 生命周期钩子函数
  if (
    (vnodeHook = props && props.onVnodeMounted) ||
    needCallTransitionHooks ||
    dirs
  ) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
      // 调用 transition.enter 钩子,并把 DOM 元素作为参数传递
      needCallTransitionHooks && transition!.enter(el)
      dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
    }, parentSuspense)
  }
}

可以看到,在 mountElement 函数中:

  1. 如果DOM节点可复用,则调用 hostCloneNode 函数,对DOM节点进行复用,否则就调用 hostCreateElement 函数创建一个新的 DOM 节点,然后将其存储到虚拟节点的 el 属性上。
  2. 然后判断子节点的类型,如果子节点是文本,则调用 hostSetElementText 函数创建文本内容并将其插入到DOM节点中。如果子节点是一个数组,则调用 mountChildren 函数批量挂载子节点。
  3. 接下来设置DOM节点上的指令、props、attr 属性等。
  4. 最后是挂载 DOM 元素。在挂载DOM元素之前,判断该DOM元素上是否有过渡动效,如果有,则执行动画的 beforeEnter 生命周期钩子函数。然后调用 hostInsert 挂载 DOM 元素,挂载完 DOM 元素之后,执行动画的 enter 生命周期钩子函数,并将 DOM 元素作为参数传递。

mountChildren 挂载子节点

// packages/runtime-core/src/renderer.ts

const mountChildren: MountChildrenFn = (
  children,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized,
  start = 0
) => {
  for (let i = start; i < children.length; i++) {
    const child = (children[i] = optimized
      ? cloneIfMounted(children[i] as VNode)
      : normalizeVNode(children[i]))
    // 递归调用 patch 函数,对子节点执行 diff 过程
    patch(
      null,
      child,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

从上面的代码中可以看到,在执行 mountChildren 挂载子节点上,实际上就是递归调用 patch 函数来对子节点执行 Diff 过程,对子节点进行挂载。

patchElement 更新 Element 节点

// packages/runtime-core/src/renderer.ts

const patchElement = (
  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean // 是否启用 diff 优化
) => {
  // 获取真实 DOM 元素,同时将新虚拟节点的 el 指向真实 DOM
  const el = (n2.el = n1.el!)
  let { patchFlag, dynamicChildren, dirs } = n2
  // #1426 take the old vnode's patch flag into account since user may clone a
  // compiler-generated vnode, which de-opts to FULL_PROPS
  patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
  const oldProps = n1.props || EMPTY_OBJ
  const newProps = n2.props || EMPTY_OBJ
  let vnodeHook: VNodeHook | undefined | null

  // disable recurse in beforeUpdate hooks
  // 在 beforeUpdate 生命周期钩子函数中禁用 递归
  parentComponent && toggleRecurse(parentComponent, false)
  if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
    invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
  }
  if (dirs) {
    invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
  }
  // 在 beforeUpdate 生命周期钩子函数中启用 递归
  parentComponent && toggleRecurse(parentComponent, true)

  // 开发环境下,热更新,需要强制执行 diff
  if (__DEV__ && isHmrUpdating) {
    // HMR updated, force full diff
    patchFlag = 0
    optimized = false
    dynamicChildren = null
  }

  const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
  // 存在动态子节点,对动态子节点执行 diff 过程
  if (dynamicChildren) {
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds
    )
    if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
      traverseStaticChildren(n1, n2)
    }
  } else if (!optimized) {
    // 不启用 diff 优化,那么所有子节点都要执行 diff 过程
    // full diff
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds,
      false
    )
  }

  if (patchFlag > 0) {
    // the presence of a patchFlag means this element's render code was
    // generated by the compiler and can take the fast path.
    // in this path old node and new node are guaranteed to have the same shape
    // (i.e. at the exact same position in the source template)
    if (patchFlag & PatchFlags.FULL_PROPS) {
      // element props contain dynamic keys, full diff needed

      // 对 props 执行 diff
      patchProps(
        el,
        n2,
        oldProps,
        newProps,
        parentComponent,
        parentSuspense,
        isSVG
      )
    } else {
      // class
      // this flag is matched when the element has dynamic class bindings.
      // 具有动态的 class,更新 class
      if (patchFlag & PatchFlags.CLASS) {
        if (oldProps.class !== newProps.class) {
          hostPatchProp(el, 'class', null, newProps.class, isSVG)
        }
      }

      // style
      // this flag is matched when the element has dynamic style bindings
      // 动态的 style,更新 style
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
      }

      // props
      // This flag is matched when the element has dynamic prop/attr bindings
      // other than class and style. The keys of dynamic prop/attrs are saved for
      // faster iteration.
      // Note dynamic keys like :[foo]="bar" will cause this optimization to
      // bail out and go through a full diff because we need to unset the old key

      // 处理动态属性/动态属性绑定
      if (patchFlag & PatchFlags.PROPS) {
        // if the flag is present then dynamicProps must be non-null
        // 需要更新的动态 props
        /**
         * propsToUpdate是 onClick | onUpdate:modelValue 这些。
         * 示例:<polygon :points="points"></polygon>  propsToUpdate === ["points"]
        */
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          // 这里的 next 可能是 string、number、function、object、boolean
          // #1471 force patch value
          // props 的值不同,则执行更新
          // 如果 props 是 value,则需要强制执行更新
          if (next !== prev || key === 'value') {
            // 更新 props
            hostPatchProp(
              el,
              key,
              prev,
              next,
              isSVG,
              n1.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
      }
    }

    // text
    // This flag is matched when the element has only dynamic text children.
    // 更新动态文本节点
    if (patchFlag & PatchFlags.TEXT) {
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children as string)
      }
    }
  } else if (!optimized && dynamicChildren == null) {
    // 不启用 diff 优化,并且没用动态子节点,所有 props 要执行更新
    // unoptimized, full diff
    patchProps(
      el,
      n2,
      oldProps,
      newProps,
      parentComponent,
      parentSuspense,
      isSVG
    )
  }

  if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
      dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
    }, parentSuspense)
  }
}

patchElement 函数主要用来更新 Element 节点,从上面的源码中可以看到:

  1. 首先从旧虚拟节点的 el 属性上获取该虚拟节点对应的真实 DOM 元素,并将新虚拟节点的 el 属性也指向该真实DOM元素。
  2. 然后判断新虚拟节点 n2 上是否存在动态子节点,如果存在,则调用 patchBlockChildren 函数对动态子节点执行 Diff 过程。如果在 Diff 的过程中没有启用 Diff 优化,则直接调用 patchChildren 函数更新所有子节点。
  3. 接着分别对Element 节点的动态属性 props、class、style以及动态的text进行更新。

patchBlockChildren 更新动态子节点

// packages/runtime-core/src/renderer.ts

const patchBlockChildren: PatchBlockChildrenFn = (
  oldChildren,
  newChildren,
  fallbackContainer,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds
) => {
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i]
    const newVNode = newChildren[i]
    // Determine the container (parent element) for the patch.
    const container =
      // oldVNode may be an errored async setup() component inside Suspense
      // which will not have a mounted element
      oldVNode.el &&
      // - In the case of a Fragment, we need to provide the actual parent
      // of the Fragment itself so it can move its children.
      (oldVNode.type === Fragment ||
        // - In the case of different nodes, there is going to be a replacement
        // which also requires the correct parent container
        !isSameVNodeType(oldVNode, newVNode) ||
        // - In the case of a component, it could contain anything.
        oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
        ? hostParentNode(oldVNode.el)!
        : // In other cases, the parent container is not actually used so we
          // just pass the block element here to avoid a DOM parentNode call.
          fallbackContainer
    // 递归调用 patch 对动态子节点执行 diff 
    patch(
      oldVNode,
      newVNode,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      true
    )
  }
}

在 patchElement 函数中,如果新的虚拟节点 n2 上存在动态子节点,就会调用 patchBlockChildren 函数对动态子节点进行更新。从 patchBlockChildren 的源码可以看到,在对动态子节点进行更新时,实际上是递归调用 patch 函数来对动态子节点执行 Diff 过程,对动态子节点进行更新。

patchChildren 更新子节点

// packages/runtime-core/src/renderer.ts

const patchChildren: PatchChildrenFn = (
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized = false
) => {
  // 旧节点的子节点
  const c1 = n1 && n1.children
  // 旧节点的 shapeFlag
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  // 新节点的子节点
  const c2 = n2.children

  const { patchFlag, shapeFlag } = n2
  // fast path
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      // this could be either fully-keyed or mixed (some keyed some not)
      // presence of patchFlag means children are guaranteed to be arrays
      patchKeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      return
    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      // unkeyed
      patchUnkeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      return
    }
  }

  // children has 3 possibilities: text, array or no children.
  // 新子节点有 3 中可能:文本、数组、或没有 children
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {

    // 文本节点的快速 diff

    // text children fast path
    // 旧子节点是数组,则卸载旧子节点
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    // 子节点是文本节点,新旧文本不一致时,直接更新
    if (c2 !== c1) {
      hostSetElementText(container, c2 as string)
    }
  } else {

      // 子节点是数组时,对子节点进行 diff 

    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {

      // 旧子节点是数字
      // prev children was array
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 新子节点也是数组,对两组子节点进行 diff
        // two arrays, cannot assume anything, do full diff
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // no new children, just unmount old
        // 旧子节点是数组时,没有新的子节点,删除旧子节点
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
      }
    } else {
      // prev children was text OR null
      // new children is array OR null
      // 旧子节点是文本或者 null
      // 新子节点是数组或者为null
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, '')
      }
      // mount new if array
      // 新子节点是数组,则挂载新子节点
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
    }
  }
}

在 patchElement 函数中,如果参数 optimized 的值为 false,即不启用 Diff 优化,那么就会调用 patchChildren 函数对所有子节点执行 diff 过程,对子节点进行更新。在 patchChildren 函数中,开始涉及到 patch 过程中的核心 —— Diff 算法,这部分内容我们放在下一篇文章中详细解读。

processComponent 处理组件

// packages/runtime-core/src/renderer.ts

const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  n2.slotScopeIds = slotScopeIds
  if (n1 == null) {
    // 首次挂载时,// 判断当前要挂载的组件是否是 KeepAlive 组件
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      // 激活组件,即将隐藏容器中移动到原容器中
      ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      // 不是 KeepAlive 组件,调用 mountComponent 挂载组件
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  } else {
    // 更新阶段,直接更新组件
    updateComponent(n1, n2, optimized)
  }
}

从 processComponent 函数的源码中可以看到,在首先渲染 DOM 树时,需要判断当前挂载的组件是否是 KeepAlive 组件,如果是,则调用 KeepAlive 组件的内部方法 activate 方法激活组件,也就是将组将从隐藏容器中移动到原容器(页面) 中。如果不是 KeepAlive 组件,则调用 mountComponent 函数挂载组件。

而在更新阶段,则是直接调用 updateComponent 函数更新组件。

mountComponent 挂载组件

// packages/runtime-core/src/renderer.ts

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 2.x compat may pre-create the component instance before actually
  // mounting
  // 2.x 版本中可能在实际操作之前已经创建了组件实例
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component

  // 1、创建组件实例
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

  // 开发环境下注册热更新
  if (__DEV__ && instance.type.__hmrId) {
    registerHMR(instance)
  }

  if (__DEV__) {
    pushWarningContext(initialVNode)
    startMeasure(instance, `mount`)
  }

  // inject renderer internals for keepAlive
  // 如果初始化的VNode是 KeepAlive 组件,则在组件实例的上下文中注入 renderer
  if (isKeepAlive(initialVNode)) {
    ;(instance.ctx as KeepAliveContext).renderer = internals
  }

  // resolve props and slots for setup context
  if (!(__COMPAT__ && compatMountInstance)) {
    if (__DEV__) {
      startMeasure(instance, `init`)
    }

    // 设置组件实例
    则在组件实例的上下文中注入 renderer(instance)

    if (__DEV__) {
      endMeasure(instance, `init`)
    }
  }

  // setup() is async. This component relies on async logic to be resolved
  // before proceeding
  if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
    parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

    // Give it a placeholder if this is not hydration
    // TODO handle self-defined fallback
    if (!initialVNode.el) {
      const placeholder = (instance.subTree = createVNode(Comment))
      processCommentNode(null, placeholder, container!, anchor)
    }
    return
  }

  // 设置并且运行带有副作用的渲染函数
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )

  if (__DEV__) {
    popWarningContext()
    endMeasure(instance, `mount`)
  }
}

在 mountComponent 函数中,做了以下事情:

  1. 首先是调用 createComponentInstance 函数创建组件实例。
  2. 然后判断即将挂载的组件是否是 KeepAlive 组件,如果是,则在组件实例的上下文中注入 renderer
  3. 接着设置组件实例,调用 setupComponent 函数初始化组件的 props、slots 。
  4. 最后调用 setupRenderEffect 函数,执行带有副作用的渲染函数。

setupComponent 初始化组件实例

// packages/runtime-core/src/component.ts

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  // 是否是状态型组件
  const isStateful = isStatefulComponent(instance)
  // 初始化 props
  initProps(instance, props, isStateful, isSSR)
  // 初始化 slots
  initSlots(instance, children)

   // 仅为状态型组件挂载setup信息,非状态型组件仅为纯UI展示不需要挂载状态信息
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

可以看到,在 setupComponent 函数中:

  1. 首先调用 isStatefulComponent 函数判断当前组件是否是状态型组件。
  2. 然后分别初始化组件的 props、slots。
  3. 接下来执行 setupStatefulComponent 函数,为状态型组件挂载 setup 信息,而非状态型组件仅为纯UI展示,不需要挂载状态信息,因此此时 setupResult 的值应设置为 undefined。
  4. 最后将 setup 信息返回。

setupStatefulComponent 生成 setup 信息

// packages/runtime-core/src/component.ts

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // 组件的 options,也就是 vnode.type
  const Component = instance.type as ComponentOptions

  if (__DEV__) {
    if (Component.name) {
      // 校验组件名称是否合法
      validateComponentName(Component.name, instance.appContext.config)
    }
    if (Component.components) {
      // 批量校验组件名称是否合法
      const names = Object.keys(Component.components)
      for (let i = 0; i < names.length; i++) {
        validateComponentName(names[i], instance.appContext.config)
      }
    }
    if (Component.directives) {
      // 批量校验指令名称是否合法
      const names = Object.keys(Component.directives)
      for (let i = 0; i < names.length; i++) {
        validateDirectiveName(names[i])
      }
    }
    if (Component.compilerOptions && isRuntimeOnly()) {
      warn(
        `"compilerOptions" is only supported when using a build of Vue that ` +
          `includes the runtime compiler. Since you are using a runtime-only ` +
          `build, the options should be passed via your build tool config instead.`
      )
    }
  }
  // 0. create render proxy property access cache
  // 创建渲染代理属性访问缓存
  instance.accessCache = Object.create(null)
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  // 为组件实例创建渲染代理,同时将代理标记为 raw,
  // 为的是在后续过程中不会被误转化为响应式数据,
  // 渲染代理源对象是组件实例上下文
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
  if (__DEV__) {
    exposePropsOnRenderContext(instance)
  }
  // 2. call setup()
  // 调用 setup 函数
  // 这里的setup是开发者调用 createApp 时传入的 setup 函数
  const { setup } = Component
  if (setup) {
    // 创建 setup上下文并挂载到组件实例上
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    // 记录当前正在初始化的组件实例
    setCurrentInstance(instance)

    // 执行 setup 前暂停依赖收集
    // PS: 执行setup期间是不允许进行依赖收集的,setup只是为了获取需要为组件提供的状态信息,在它里面不应该有其它非必要的副作用
    // 真正的依赖收集等有较强副作用的操作应该放到 setup挂载之后,以免产生不可预测的问题
    pauseTracking()
    // 执行 setup 函数,并获得安装结果信息,setup执行结构就是我们定义的响应式数据、函数、钩子等
    const setupResult = callWithErrorHandling(
      setup, // 开发者调用 createApp 时定义的 setup函数
      instance, // 根组件实例
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )

    // setup执行完毕后恢复依赖收集
    resetTracking()

    // 重置当前根组件实例
    unsetCurrentInstance()

    // 挂载 setup 执行的结果
    // 在 SSR 服务端渲染或者 suspense 时 setup 返回的是 promise
    // 因此需要判断 setupResult 是否是 promise,进行不同的操作
    if (isPromise(setupResult)) {
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance)

      if (isSSR) {
        // return the promise so server-renderer can wait on it
        // 在SSR或者suspense时setup返回promise
        // suspense因为有节点fallback,而setup中是正式渲染内容,因此是一个异步resolve的过程
        return setupResult
          .then((resolvedResult: unknown) => {
            handleSetupResult(instance, resolvedResult, isSSR)
          })
          .catch(e => {
            handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
          })
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.
        instance.asyncDep = setupResult
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        )
      }
    } else {
      // setupResult 返回的不是 promise
      // 直接将 setup的执行结果挂载到组件实例上
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

从 setupStatefulComponent 函数的源码中可以看到,在开发环境下,会校验祖静的名称已经指令名称是否合法。

  1. 接下来为组件实例创建一个渲染代理属性accessCache,用于访问缓存。
  2. 接着继续为组件创建一个渲染代理 proxy,并同时经代理标记为 raw,为的是在后续过程中不会被误转化为响应式数据。渲染代理的源对象是组件实例的上下文对象。
  3. 接下来调用 setup 函数生成setup信息,这里的 setup 函数,就是开发者在调用 createApp 时传入的 setup 函数。
  4. 在执行 setup 的过程中,首先创建一个 setup 上下文对象,并将其挂载到组件实例上,然后调用 setCurrentInstance 函数记录当前正在初始化的组件实例。
  5. 在执行 setup 函数之前,需要先暂停依赖收集,原因是 setup 只是为了获取需要为组件提供的状态信息,在它里面不应该有其它非必要的副作用, 而真正的依赖收集等有较强副作用的操作应该放到 setup挂载之后,以免产生不可预测的问题。
  6. 在暂停依赖收集之后,执行 setup 函数,获得组件安装信息,这些安装信息就是开发者定义的响应式数据、函数、钩子等。
  7. setup 执行完毕后需要恢复依赖收集,因此调用 resetTracking 函数恢复依赖收集,并调用 unsetCurrentInstance 函数重置当前的组件实例。
  8. 如果是在服务端渲染或者是在 Suspense 组件中,setup 执行后返回的结果是一个 promise,因此我们还需要根据 setup 的结果是否是 promise ,执行不同的操作。如果是 promise ,则执行 promise 的 then 函数,获取真正的setup信息,将其挂载到组件实例上。如果不是 promise,则直接将 setup 执行后的结果挂载到组件实例上。

updateComponent 更新组件

// packages/runtime-core/src/renderer.ts

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  // 从旧虚拟节点上获取组件实例,并将组件实例添加到新虚拟节点上
  const instance = (n2.component = n1.component)!

  // 根据新旧虚拟节点VNode上的属性、指令、子节点等判断是否需要更新组件
  // optimized 参数用于设置是否开启 diff 优化
  if (shouldUpdateComponent(n1, n2, optimized)) {
    if (
      __FEATURE_SUSPENSE__ &&
      instance.asyncDep &&
      !instance.asyncResolved
    ) {
      // async & still pending - just update props and slots
      // since the component's reactive effect for render isn't set-up yet
      if (__DEV__) {
        pushWarningContext(n2)
      }
      // 异步组件,预更新组件
      updateComponentPreRender(instance, n2, optimized)
      if (__DEV__) {
        popWarningContext()
      }
      return
    } else {
      // normal update
      // 更新对应组件实例的 next 为新的 VNode
      instance.next = n2
      // in case the child component is also queued, remove it to avoid
      // double updating the same child component in the same flush.
      invalidateJob(instance.update)
      // instance.update is the reactive effect.
      // 触发更新
      instance.update()
    }
  } else {
    // no update needed. just copy over properties
    // 不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上
    n2.component = n1.component
    n2.el = n1.el
    instance.vnode = n2
  }
}

在 updateComponent 函数中,首先从旧虚拟节点 n1 的 component 属性获取当前需要更新的组件实例,并将该组件实例存储到新虚拟节点 n2 的 component 属性上,保持对组件实例的引用。

然后根据新旧 vnode 上的 props、指令、子节点等判断是否需要更新组件,如果需要更新组件,则调用组件实例的 update 方法触发更新。如果不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上即可。

Teleport.process 渲染 Teleport 组件

else if (shapeFlag & ShapeFlags.TELEPORT) {
  // 处理 Teleport 组件
  // 调用 Teleport 组件内部的 process 函数,渲染 Teleport 组件的内容
  ;(type as typeof TeleportImpl).process(
    n1 as TeleportVNode,
    n2 as TeleportVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized,
    internals
  )
} 

如上面的代码所示,如果 shapeFlag 的类型为 ShapeFlags.TELEPORT ,则调用 Teleport 组件内部的 process 函数,渲染 Teleport 组件的内容。Teleport 组件的源码解读请阅读《Vue3 源码解读之 Teleport 组件》一文。

Suspense.process 渲染 Suspense 组件

else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理 Suspense 组件
// 调用 Suspense 组件内部的 process 函数,渲染 Suspense 组件的内容
;(type as typeof SuspenseImpl).process(
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized,
  internals
)

如上面的代码所示,如果 shapeFlag 的类型为 ShapeFlags.SUSPENSE ,则调用 Suspense 组件内部的 process 函数,渲染 Suspense 组件的内容。

总结

本文主要介绍了 patch 过程中的文本节点、注释节点、静态节点、Fragment节点、Element类型的节点、Component 组件、Teleport 组件、Suspense 异步组件等处理过程。

在调用 patchElement 更新 Element 类型的节点时,会调用 patchChildren 对子节点进行更新,在对子节点进行更新的过程中,需要对新旧子节点进行 Diff 比较。对于 Diff 算法的详细解读将放在下《Vue3 源码解读之patch算法(二)》一文中。