Vue3源码通过render patch 了解diff

引言

上一篇中,我们理清了createApp走的流程,最后通过createAppAPI创建了app。虽然app上的各种属性和方法也都已经有所了解,但其中的mountunmount方法,都是通过调用render函数来完成的。尽管我们很好奇render函数的故事,可是baseCreateRenderer函数有2000+行,基本都和render相关,因此拆解到本文里叙述,以下方法都定义在baseCreateRenderer函数中。

render

render也不神秘了,毕竟在上一篇文章中露过面,当然这里也顺带提一下 baseCreateRenderer从参数options中解构的一些方法,基本都是些增删改查、复制节点的功能,见名知义了。主要看看render,接收vnodecontainerisSvg三个参数。调用unmount卸载或者调用patch进行节点比较从而继续下一步。

  • 判断vnode是否为null。如果对上一篇文章还有印象,那么就会知道,相当于是判断调用的是app.mount还是app.unmount方法,因为app.unmount方法传入的vnode就是null。那么这里对应的就是在app.unmount里使用unmount函数来卸载;而在app.mount里进行patch比较。
  • 调用flushPostFlushCbs(),其中的单词Post的含义,看过第一篇讲解watch的同学也许能猜出来,表示执行时机是在组件更新后。这个函数便是执行组件更新后的一些回调。
  • vnode挂到container上,即旧的虚拟DOM
const {
  insert: hostInsert,
  remove: hostRemove,
  patchProp: hostPatchProp,
  createElement: hostCreateElement,
  createText: hostCreateText,
  createComment: hostCreateComment,
  setText: hostSetText,
  setElementText: hostSetElementText,
  parentNode: hostParentNode,
  nextSibling: hostNextSibling,
  setScopeId: hostSetScopeId = NOOP,
  cloneNode: hostCloneNode,
  insertStaticContent: hostInsertStaticContent
} = options
// render
const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 新旧节点的对比
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  // 记录旧节点
  container._vnode = vnode
}

patch

patch函数里主要对新旧节点也就是虚拟DOM的对比,常说的vue里的diff算法,便是从patch开始。结合render函数来看,我们知道,旧的虚拟DOM存储在container._vnode上。那么diff的方式就在patch中了:

新旧节点相同,直接返回;

旧节点存在,且新旧节点类型不同,则旧节点不可复用,将其卸载(unmount),锚点anchor移向下一个节点;

新节点是否静态节点标记;

根据新节点的类型,相应地调用不同类型的处理方法:

  • 文本:processText
  • 注释:processCommentNode
  • 静态节点:mountStaticNodepatchStaticNode
  • 文档片段:processFragment
  • 其它。

在 其它 这一项中,又根据形状标记 shapeFlag等,判断是 元素节点、组件节点,或是TeleportSuspense等,然后调用相应的process去处理。最后处理template中的ref

// Note: functions inside this closure should use `const xxx = () => {}`
// style in order to prevent being inlined by minifiers.
const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
  // 新旧节点相同,直接返回
  if (n1 === n2) {
    return
  }
  // 旧节点存在,且新旧节点类型不同,卸载旧节点,锚点anchor后移
  // patching & not same type, unmount old tree
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }
  // 是否静态节点优化
  if (n2.patchFlag === PatchFlags.BAIL) {
    optimized = false
    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:
      processFragment(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        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) {
        ;(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) {
        ;(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})`)
      }
  }
  // 处理 template 中的 ref 
  // set ref
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
  }
}

processText

文本节点的处理十分简单,没有旧节点则新建并插入新节点;有旧节点,且节点内容不一致,则设置为新节点的内容。

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)
    }
  }
}

 processCommontNode

不支持动态的注视节点,因此只要旧节点存在,就使用旧节点的内容。

const processCommentNode: ProcessTextOrCommentFn = (
    n1,
    n2,
    container,
    anchor
  ) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateComment((n2.children as string) || '')),
        container,
        anchor
      )
    } else {
      // there's no support for dynamic comments
      n2.el = n1.el
    }
  }

mountStaticNode 和 patchStaticNode

事实上静态节点没啥好比较的,毕竟是静态的。当没有旧节点时,则通过mountStaticNode创建并插入新节点;即使有旧节点,也仅在_DEV_条件下在hmr,才会使用patchStaticVnode做一下比较并通过removeStaticNode移除某些旧节点。

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.
  ;[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 {
    n2.el = n1.el
    n2.anchor = n1.anchor
  }
}
// removeStaticNode:从 n1.el 至 n1.anchor 的内容被遍历移除
const removeStaticNode = ({ el, anchor }: VNode) => {
  let next
  while (el && el !== anchor) {
    next = hostNextSibling(el)
    hostRemove(el)
    el = next
  }
  hostRemove(anchor!)
}

processFragment

vue3的单文件组件里,不再需要加一个根节点,因为使用了文档片段fragment来承载子节点,最后再一并添加到文档中。

若旧的片段节点为空,则插入起始锚点,挂载新的子节点;

旧的片段不为空:

  • 存在优化条件时:使用patchBlockChildren优化diff
  • 不存在优化条件时:使用patchChildren进行全量diff
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
  // 开发环境热更新时,强制全量diff
  if (
    __DEV__ &&
    // #5523 dev root fragment may inherit directives
    (isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
  ) {
    // HMR updated / Dev root fragment (w/ comments), 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) {
    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 {
    // 当旧片段不为空时,启用优化则使用patchBlockChildren
    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