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

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

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

patch 函数

patch源码

js 复制代码
// 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
  }

  // 新旧虚拟节点不相同(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 处理文本节点

processText源码

js 复制代码
// packages/runtime-core/src/renderer.ts

// 处理文本 n1:旧虚拟节点 n2:新虚拟节点
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 中,同时将文本元素存储到新虚拟节点 n2el 属性上,保持对真实DOM元素的引用。

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

processCommentNode 处理注释节点

processCommentNode源码

js 复制代码
// 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 处理静态节点

patchStaticNode源码

js 复制代码
// packages/runtime-core/src/renderer.ts

// 挂载静态节点
const mountStaticNode = (
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  isSVG: boolean
) => {
  // 判断是否存在 hostInsertStaticContent 方法,该方法用于将静态节点插入到容器中。该方法只在使用 compiler-dom/runtime-dom 时才存在,因为这两个模块是用于浏览器环境的。
  // 向 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
) => {
  // 在开发环境下,如果修改了静态节点的内容,需要将其更新到页面上。为了提高性能,只有修改了静态节点的内容才会触发更新,而不是每次都重新渲染静态节点。
  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 元素

js 复制代码
// 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
  }

  // 插槽是一种组件通信机制,用于在父组件中定义模板,并在子组件中渲染。在处理插槽时,编译器需要根据插槽的特点来进行一些处理,例如提取作用域 ID
  // 如果存在插槽片段的作用域 ID,则将其添加到插槽作用域 ID 数组中。
  if (fragmentSlotScopeIds) {
    slotScopeIds = slotScopeIds
      ? slotScopeIds.concat(fragmentSlotScopeIds)
      : fragmentSlotScopeIds
  }

  if (n1 == null) {
    // 首次挂载时插入 Fragment
    hostInsert(fragmentStartAnchor, container, anchor)
    hostInsert(fragmentEndAnchor, container, anchor)
    // 断言片段节点的子节点为数组类型,因为片段节点只能包含数组子节点。
    // 挂载子节点,这里只能是数组的子集
    mountChildren(
      n2.children as VNodeArrayChildren,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    if (
      patchFlag > 0 &&
      patchFlag & PatchFlags.STABLE_FRAGMENT &&
      dynamicChildren &&
      // 如果旧节点存在动态子节点,则说明之前的片段可能是因为 renderSlot() 没有有效子节点而被跳过的
      n1.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 判断新节点是否具有 key 属性。如果存在 key 属性,则说明该片段是一个带有 v-for 的模板,它的根节点可能会被移动,因此需要为其设置 el 属性
        // #2134 判断新节点是否为组件的根节点。如果是组件的根节点,则说明该片段是一个组件的子树,它的根节点也可能会被移动,因此需要为其设置 el 属性。
        n2.key != null ||
        (parentComponent && n2 === parentComponent.subTree)
      ) {
        // 转换静态子节点
        traverseStaticChildren(n1, n2, true /* shallow */)
      }
    } else {
      // 首先判断片段的子节点是否是编译器生成的。如果是编译器生成的,则子节点一定是块级别的节点,因此片段不会有动态子节点。

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

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

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

processElement 处理 Element

processElement

js 复制代码
// 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 节点

mountElement源码

js 复制代码
// 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
  ) {
    // 首先判断虚拟节点的 el 属性是否存在。如果存在,则说明该虚拟节点是一个被复用的静态节点。
    // 接下来,将虚拟节点的 el 属性赋值给 el 变量,并将虚拟节点的 el 属性设置为克隆节点的 el 属性。这样就可以复用静态节点了。
    // 复用静态节点
    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')
  }
  // 先判断 suspense 是否已经解析出来。如果已经解析出来,则直接调用 enter 钩子函数。
  // 如果 suspense 还没有解析出来,则需要等待 suspense 解析出来之后再调用 enter 钩子函数。在这种情况下,需要在 suspense 解析出来之后再调用 enter 钩子函数
  // 执行动画相关的生命周期钩子
  // 判断一个 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. 然后先判断 suspense 是否已经解析出来。如果已经解析出来,则直接调用 enter 钩子函数。如果 suspense 还没有解析出来,则需要等待 suspense 解析出来之后再调用 enter 钩子函数。在这种情况下,需要在 suspense 解析出来之后再调用 enter 钩子函数。
  5. 最后是挂载 DOM 元素。在挂载DOM元素之前,判断该DOM元素上是否有过渡动效,如果有,则执行动画的 beforeEnter 生命周期钩子函数。然后调用 hostInsert 挂载 DOM 元素,挂载完 DOM 元素之后,执行动画的 enter 生命周期钩子函数,并将 DOM 元素作为参数传递。

mountChildren 挂载子节点

js 复制代码
// 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 节点

patchElement源码

js 复制代码
// 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 为什么需要考虑旧 vnode 的 patch flag?这是因为用户可能会克隆编译器生成的 vnode,从而使其 de-opt 到 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) {
    // 首先判断元素的 patchFlag 是否包含 FULL_PROPS 标记。如果包含该标记,则可以采用快速更新的方式来更新元素。
    // 接着,判断新旧节点是否具有相同的结构。如果节点结构相同,则可以直接更新元素的属性。否则,需要重新创建元素并更新属性。
    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
      // 元素的 prop/attr 可以是静态的或动态的。静态的 prop/attr 在编译期间就可以确定,而动态的 prop/attr 只能在运行时确定。在处理元素时,编译器需要根据元素的 prop/attr 是否动态来决定如何进行更新。
      // 1、当元素具有除 class 和 style 之外的动态 prop/attr 绑定时,会设置的 patch flag。该标记表示元素具有动态 prop/attr 绑定,需要进行更新
      // 2、解释了动态 prop/attr 绑定的键会被保存,以便快速遍历。这是因为在运行时,需要遍历动态 prop/attr 绑定的键来确定哪些 prop/attr 是动态的,从而进行更新
      // 3、最后,当动态 prop/attr 绑定的键是动态的时,会放弃该优化,并执行全量更新。这是因为在更新时,需要取消旧键,再设置新键,无法通过快速更新来实现

      // 处理动态属性/动态属性绑定
      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 更新动态子节点

js 复制代码
// 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]
    // 先判断旧 vnode 是否具有 mounted element。如果有,则将其作为容器。否则,使用 fallbackContainer 作为容器。
    // 接着,判断旧 vnode 的类型。如果旧 vnode 是 Fragment,则需要提供 Fragment 自身的实际父级元素作为容器,以便移动其子元素。如果旧 vnode 的类型与新 vnode 不同,则需要提供正确的父级容器。如果旧 vnode 是组件或 teleport,则其子元素可能包含任何内容,因此需要提供正确的父级容器。
    // 最后,如果旧 vnode 的父级容器实际上没有使用,则只需将块元素传递给 host 函数,以避免调用 DOM parentNode
    
    // 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
    )
  }
}
  • patch 是指将新的 vnode 更新到旧的 vnode 上的过程。在进行 patch 时,需要确定新 vnode 的容器(父元素)
    • 先判断旧 vnode 是否具有 mounted element。如果有,则将其作为容器。否则,使用 fallbackContainer 作为容器。
    • 接着,判断旧 vnode 的类型。如果旧 vnodeFragment,则需要提供 Fragment 自身的实际父级元素作为容器,以便移动其子元素。如果旧 vnode 的类型与新 vnode 不同,则需要提供正确的父级容器。如果旧 vnode 是组件或 teleport,则其子元素可能包含任何内容,因此需要提供正确的父级容器。
    • 最后,如果旧 vnode 的父级容器实际上没有使用,则只需将块元素传递给 host 函数,以避免调用 DOM parentNode
  • patchElement 函数中,如果新的虚拟节点 n2 上存在动态子节点,就会调用 patchBlockChildren 函数对动态子节点进行更新。从 patchBlockChildren 的源码可以看到,在对动态子节点进行更新时,实际上是递归调用 patch 函数来对动态子节点执行 Diff 过程,对动态子节点进行更新。

patchChildren 更新子节点

patchChildren源码

js 复制代码
// 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) {
      // 先判断子节点是否具有 key。如果子节点中至少有一个 vnode 具有 key,则调用 patchKeyedChildren 函数,对具有 key 的子节点进行更新。
      // 接着,传递需要更新的子节点数组、新的子节点数组、容器、锚点、父组件、父 suspense、是否为 SVG、插槽作用域 ID、是否进行优化等参数给 patchKeyedChildren 函数。
      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
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // 旧子节点是数组时,没有新的子节点,那就卸载旧子节点
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
      }
    } else {
      // 旧子节点是文本或者 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
        )
      }
    }
  }
}

子节点可以是具有 keyvnode 或没有 keyvnode。具有相同 keyvnode 被视为相同的 vnode,可以采用快速更新的方式来更新它们。而没有 keyvnode 则需要进行全量更新。

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

processComponent 处理组件

processComponent源码

js 复制代码
// 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 挂载组件

mountComponent源码

js 复制代码
// packages/runtime-core/src/renderer.ts

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 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`)
  }

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

  // 在初始化组件实例时,需要解析 setup 函数的参数,以便将 props 和 slots 注入到组件实例中。
  if (!(__COMPAT__ && compatMountInstance)) {
    // 如果是开发环境,则使用 startMeasure 和 endMeasure 函数计时。
    if (__DEV__) {
      startMeasure(instance, `init`)
    }
    
   // 调用 setupComponent 函数,解析 setup 函数的参数并将其注入到组件实例中。
    setupComponent(instance)
    if (__DEV__) {
      endMeasure(instance, `init`)
    }
  }
  if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
    parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

    // 如果 initialVNode.el 不存在,则需要创建占位符。
    // 接着,创建一个 Comment 类型的 vnode 作为占位符,并将其赋值给 instance.subTree。接着,调用 processCommentNode 函数,将占位符 vnode 处理为 Comment 类型的 DOM 节点,并将其插入到容器中。
    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
    1. 因为组件实例的 propsslots 可以通过 setup 函数的参数来访问。在初始化组件实例时,需要解析 setup 函数的参数,以便将 propsslots 注入到组件实例中。
  4. 最后调用 setupRenderEffect 函数,执行带有副作用的渲染函数。

setupComponent 初始化组件实例

setupComponent源码

js 复制代码
// 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 信息返回。

有的同学问了,为什么要区分状态型组件和非状态型组件?

这里就需要了解下状态型组件和非状态型组件的区别了:

  • 状态型组件:是指具有响应式状态的组件,即具有响应式数据、计算属性、侦听器等。这些组件需要在组件挂载时调用 setup 函数来初始化响应式状态,并在组件更新时重新计算响应式状态。为了提高性能,Vue3 会对状态型组件进行优化,以减少不必要的计算和更新。
  • 非状态型组件:是指没有响应式状态的组件,即只有静态数据和方法的组件。这些组件不需要调用 setup 函数来初始化响应式状态,只需要在组件挂载时渲染静态内容即可。为了降低内存使用,Vue3 不会为非状态型组件创建额外的响应式状态,从而减少内存占用。

因此,区分状态型组件和非状态型组件可以让 Vue3 针对不同类型的组件进行不同的优化,从而提高性能和降低内存使用

setupStatefulComponent 生成 setup 信息

setupStatefulComponent源码

js 复制代码
// 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.`
      )
    }
  }
  // 1、创建渲染代理属性访问缓存
  instance.accessCache = Object.create(null)
  // 2、为组件实例创建渲染代理,同时将代理标记为 raw,
  // 为的是在后续过程中不会被误转化为响应式数据,
  // 渲染代理源对象是组件实例上下文
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
  if (__DEV__) {
    exposePropsOnRenderContext(instance)
  }
  // 3、调用 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) {
        // 在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.
        // 如果开启了 Suspense 特性,则组件在挂载时可以使用 Suspense 组件来等待异步操作完成
        // 在组件挂载时,如果 setup 函数返回一个 Promise,则需要等待 Promise 完成后再继续挂载组件
        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 {
      // 直接将 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 ,执行不同的操作。如果是 promise,,则需要等待 Promise 完成后再继续挂载组件,执行 promise 的 then 函数,获取真正的setup信息,将其挂载到组件实例上。如果不是 promise,则直接将 setup 执行后的结果挂载到组件实例上。

updateComponent 更新组件

updateComponent源码

js 复制代码
// 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
    ) {
      // 判断组件是否仍处于异步挂载过程中,并且异步操作仍处于 pending 状态。如果是,则只更新组件的 props 和 slots,不进行渲染。这是因为在组件的 reactive effect for render 还没有设置完成之前,组件无法进行渲染。
      if (__DEV__) {
        pushWarningContext(n2)
      }
      // 异步组件,预更新组件
      updateComponentPreRender(instance, n2, optimized)
      if (__DEV__) {
        popWarningContext()
      }
      return
    } else {
      // 更新对应组件实例的 next 为新的 VNode
      instance.next = n2
      // 判断组件实例的 update 函数是否已经被排入队列中。如果是,则使用 invalidateJob 函数将其从队列中移除,以避免重复更新子组件。
      invalidateJob(instance.update)
      // 触发更新
      instance.update()
    }
  } else {
    // 不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上
    n2.component = n1.component
    n2.el = n1.el
    instance.vnode = n2
  }
}

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

2、然后根据新旧 vnode 上的 props、指令、子节点等判断是否需要更新组件,如果需要更新组件,则调用组件实例的 update 方法触发更新。这里需要注意组件是否正在挂在中做不同的处理:

  • 组件挂载中:在异步组件(异步组件是指需要等待异步操作完成后才能渲染的组件)挂载过程中,如果异步操作仍处于 pending 状态,则需要暂停组件的挂载过程,并等待异步操作完成后再继续挂载组件。在等待异步操作完成的过程中,可以更新组件的 props 和 slots,但不能进行渲染。
  • 组件挂载完成:当一个组件更新时,它的子组件也可能会被更新。如果子组件的更新也被排入队列中,那么在同一刷新中可能会重复更新同一个子组件,导致性能问题。为了避免这种情况,需要在更新子组件之前将其从队列中移除

3、如果不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上即可。

Teleport.process 渲染 Teleport 组件

js 复制代码
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 组件的内容。

Suspense.process 渲染 Suspense 组件

js 复制代码
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
)

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

总结

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

在调用 patchElement 更新 Element 类型的节点时,会调用 patchChildren 对子节点进行更新,在对子节点进行更新的过程中,需要对新旧子节点进行 Diff 比较(有点多,放到下一节分析)。

相关推荐
让开,我要吃人了2 小时前
HarmonyOS开发实战(5.0)实现二楼上划进入首页效果详解
前端·华为·程序员·移动开发·harmonyos·鸿蒙·鸿蒙系统
everyStudy4 小时前
前端五种排序
前端·算法·排序算法
甜兒.5 小时前
鸿蒙小技巧
前端·华为·typescript·harmonyos
Jiaberrr8 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy8 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白8 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、8 小时前
Web Worker 简单使用
前端
web_learning_3218 小时前
信息收集常用指令
前端·搜索引擎
tabzzz9 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百9 小时前
Vuex详解
前端·javascript·vue.js