Skip to content

应用的挂载

vue项目的入口文件,我们都需要使用createApp创建一个或多个应用实例,并调用应用实例的mount方法挂载到指定的DOM元素中。

创建实例

使用createApp创建一个应用实例。它可以接受两个参数:rootComponent(根组件)、rootProps(根组件所需的props

ts
export type CreateAppFunction<HostElement> = (
  rootComponent: Component,
  rootProps?: Data | null
) => App<HostElement>

源码位置:packages/runtime-dom/src/index.ts

ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }

    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

在前面文章中介绍渲染器时,我们知道在createApp中,首先会创建渲染器,并调用渲染器的createApp方法创建一个实例。接下来我们继续看createApp后续的处理。

在开发环境下,会调用injectNativeTagCheckinjectCompilerOptionsCheck两个方法。

ts
if (__DEV__) {
  injectNativeTagCheck(app)
  injectCompilerOptionsCheck(app)
}

其中injectNativeTagCheck方法会修改app.config.isNativeTag,一个判断tag是否为原生标签,会被用于验证组件的名称。

ts
function injectNativeTagCheck(app: App) {
  Object.defineProperty(app.config, 'isNativeTag', {
    value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),
    writable: false
  })
}

injectCompilerOptionsCheck方法主要检查编译参数的设置是否设置正确,检查的前提是isRuntimeOnly(),只在运行时时期进行检查,即不存在将模板转为渲染函数的函数compiler

ts
function injectCompilerOptionsCheck(app: App) {
  if (isRuntimeOnly()) {
    const isCustomElement = app.config.isCustomElement
    Object.defineProperty(app.config, 'isCustomElement', {
      get() {
        return isCustomElement
      },
      set() {
        warn(
          `The \`isCustomElement\` config option is deprecated. Use ` +
          `\`compilerOptions.isCustomElement\` instead.`
        )
      }
    })

    const compilerOptions = app.config.compilerOptions
    const msg =
      `The \`compilerOptions\` config option is only respected when using ` +
      `a build of Vue.js that includes the runtime compiler (aka "full build"). ` +
      `Since you are using the runtime-only build, \`compilerOptions\` ` +
      `must be passed to \`@vue/compiler-dom\` in the build setup instead.\n` +
      `- For vue-loader: pass it via vue-loader's \`compilerOptions\` loader option.\n` +
      `- For vue-cli: see https://cli.vuejs.org/guide/webpack.html#modifying-options-of-a-loader\n` +
      `- For vite: pass it via @vitejs/plugin-vue options. See https://github.com/vitejs/vite/tree/main/packages/plugin-vue#example-for-passing-options-to-vuecompiler-dom`

    Object.defineProperty(app.config, 'compilerOptions', {
      get() {
        warn(msg)
        return compilerOptions
      },
      set() {
        warn(msg)
      }
    })
  }
}

然后对appmount方法进行了重写,并返回了app。可见我们调用createAppmount方法就是此处的mount。接下来我们看应用是如何进行挂载的

应用的挂载

mount函数接收一个参数:containerOrSelector(一个容器,它可以是选择器、ShadowDom,也可以是个DOM节点)。

ts
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
  const container = normalizeContainer(containerOrSelector)
  if (!container) return

  const component = app._component
  if (!isFunction(component) && !component.render && !component.template) {
    component.template = container.innerHTML
    if (__COMPAT__ && __DEV__) {
      for (let i = 0; i < container.attributes.length; i++) {
        const attr = container.attributes[i]
        if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
          compatUtils.warnDeprecation(
            DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
            null
          )
          break
        }
      }
    }
  }

  container.innerHTML = ''
  const proxy = mount(container, false, container instanceof SVGElement)
  if (container instanceof Element) {
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
  }
  return proxy
}

因为containerOrSelector可能是的类型可能是字符串、ELementShadowRoot,所以调用normalizeContainer方法对参数进行标准化处理。

ts
function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    if (__DEV__ && !res) {
      warn(
        `Failed to mount app: mount target selector "${container}" returned null.`
      )
    }
    return res
  }
  if (
    __DEV__ &&
    window.ShadowRoot &&
    container instanceof window.ShadowRoot &&
    container.mode === 'closed'
  ) {
    warn(
      `mounting on a ShadowRoot with \`{mode: "closed"}\` may lead to unpredictable bugs`
    )
  }
  return container as any
}

如果没有找到对应的container直接return

然后获取app的根组件app._component。如果根组件不是个function,也没有对应的rendertempalte属性,会将container.innerHTML作为根组件的template属性。

ts
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
  // 将container.innerHTML作为根组件的template属性
  component.template = container.innerHTML
  // 2.x兼容
  if (__COMPAT__ && __DEV__) {
    for (let i = 0; i < container.attributes.length; i++) {
      const attr = container.attributes[i]
      if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
        compatUtils.warnDeprecation(
          DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
          null
        )
        break
      }
    }
  }
}

紧接着,将container中的内容设置为空,并调用mount方法生成一个proxy。如果container是个Element,会移除其v-cloak属性,并添加一个值为空的data-v-app属性,最后返回proxy

ts
container.innerHTML = ''
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
  container.removeAttribute('v-cloak')
  container.setAttribute('data-v-app', '')
}
return proxy

v-clock主要用于DOM内模板,在模板未编译完成之间,用户可能先看到原始双大括号标签,直到挂载的组件将它们替换为渲染的内容。所以通过添加v-cloak配合[v-cloak] { display: none }CSS将其暂时隐藏起来,等到实例挂载完成后,再将v-cloak移除。

mount

mount方法可以接收三个参数:rootContainer(根容器)、isHydrate(是否注水)、isSVG(根容器是否为SVG)

ts
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  if (!isMounted) {
    if (__DEV__ && (rootContainer as any).__vue_app__) {
      warn(
        `There is already an app instance mounted on the host container.\n` +
        ` If you want to mount another app on the same host container,` +
        ` you need to unmount the previous app by calling \`app.unmount()\` first.`
      )
    }
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    vnode.appContext = context

    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer, isSVG)
      }
    }

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    ;(rootContainer as any).__vue_app__ = app

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      app._instance = vnode.component
      devtoolsInitApp(app, version)
    }

    return getExposeProxy(vnode.component!) || vnode.component!.proxy
  } else if (__DEV__) {
    warn(
      `App has already been mounted.\n` +
      `If you want to remount the same app, move your app creation logic ` +
      `into a factory function and create fresh app instances for each ` +
      `mount - e.g. \`const createMyApp = () => createApp(App)\``
    )
  }
}

mount中首先会判断是否已经挂载,如果没过载过,则进行挂载。

在挂载过程中,会先检查rootContainer.__vue_app__属性,如果存在rootContainer.__vue_app__,说明rootContainer已经挂载一个实例了,此时会进行一个提示。

ts
if (__DEV__ && (rootContainer as any).__vue_app__) {
  warn(
    `There is already an app instance mounted on the host container.\n` +
    ` If you want to mount another app on the same host container,` +
    ` you need to unmount the previous app by calling \`app.unmount()\` first.`
  )
}

紧接着创建根组件的vnode,并将上下文对象保存到设置vnodeappContext。这里的rootComponent就是createApp时传入的rootComponent

ts
const vnode = createVNode(
  rootComponent as ConcreteComponent,
  rootProps
)
vnode.appContext = context

然后渲染vnode,如果是同构渲染使用hydrate,否在调用render进行渲染,渲染完成后,将isMounted设置为true,表示已经挂载完毕,同时将rootContainer保存到app实例的_container中,并将app实例保存在rootContainer__vue_app__属性中。

ts
if (isHydrate && hydrate) {
  hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
  render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
;(rootContainer as any).__vue_app__ = app

最后返回组件所暴露的一些属性或方法。vnode.component.proxy是组件实例this的代理对象

ts
return getExposeProxy(vnode.component!) || vnode.component!.proxy

getExposeProxy方法会返回instance.exposeProxy

ts
export function getExposeProxy(instance: ComponentInternalInstance) {
  if (instance.exposed) {
    return (
      instance.exposeProxy ||
      (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
        get(target, key: string) {
          if (key in target) {
            return target[key]
          } else if (key in publicPropertiesMap) {
            return publicPropertiesMap[key](instance)
          }
        }
      }))
    )
  }
}

render

挂载过程调用了一个render方法或hydrate进行渲染。此处我们继续看下render函数如何将vnode渲染为真实DOM的。

在介绍渲染器时,我们知道渲染器中有个createApp方法,这个方法会在创建app实例时被首先调用。createApp方法通过一个createAppAPI函数生成,这个函数接收两个参数:renderhydrate,这里的render就是在挂载过程中调用的渲染函数。

ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // ...
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

来看下render函数的实现:

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

render函数接收三个参数::vnode(需要挂载的虚拟DOM)、container(需要渲染到的容器)、isSVG(被渲染到的容器是否为SVG)

当被传入的vnodenull时,说明什么都不渲染,这时会检查container中是否存在_vnode,如果存在调用unmount卸载函数。如果传入的vnode不为null,会调用patch函数进行更新,也可以称为打补丁。最后执行flushPostFlushCbs()(如果此时有等待中的前置任务和后置任务,需要执行这些任务,如通过watchEffectwatchPostEffect添加的effect,还有mounted等钩子),并将vnode添加到container._vnode中。

由于在挂载过程中,会向render传入根组件的vnode,所以继续调用patch方法。

patch

patch函数可以接收9个参数:

  • n1:旧的vnode
  • n2:新的vnode
  • container:需要更新的容器
  • anchor:锚点
  • parentComponent:父组件
  • parentSuspense:父Suspence
  • isSVG:容器是否为SVG
  • slotScopeIds
  • optimized:是否开启优化模式
patch完整代码
ts
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
  }

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

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

首先比较n1n2,如果n1n2相同,代表着新节点没有发生更新,所以直接return。在第一次挂载过程中,由于旧vnode是空的,所以会继续进行下面的操作。

ts
if (n1 === n2) {
  return
}

如果旧节点不为空,而且新旧节点的节点类型不同,则需要卸载旧节点。

ts
if (n1 && !isSameVNodeType(n1, n2)) {
  // 获取锚点
  anchor = getNextHostNode(n1)
  // 卸载旧节点
  unmount(n1, parentComponent, parentSuspense, true)
  // 将旧节点置为空
  n1 = null
}

判断两个节点类型是否一样

比较两个节点的typekey是否一致。

ts
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as ConcreteComponent)
  ) {
    // HMR only: if the component has been hot-updated, force a reload.
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}

如果新节点的patchFlagPatchFlags.BAIL,意味着diff过程退出优化模式,这时会将optimized设置为false,并将新节点的dynamicChildren设置为null

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

接着就是根据新节点的typeshapeFlag属性进行不同的分支:

ts
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) { // 处理HTML节点
      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
      ;(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
      ;(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})`)
    }
}

在应用挂载时,这里可能进入不同分支。我们这里以createApp(ComponentXXX)为例。

render过程中,创建根vnode时,由于其typeObject,所以vnode.shapeFlag属性为ShapeFlags.STATEFUL_COMPONENTShapeFlags.COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT),所以第一次patch,会执行processComponent

processComponent

processComponent函数接收与patch相同的参数

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) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  } else {
    updateComponent(n1, n2, optimized)
  }
}

可以看到当旧节点为空时,如果新节点对应的组件已经被keep-alive了,则调用parentComponent.ctx.activate方法进行激活组件,否则调用mountComponent方法挂载组件;如果旧节点不为空,则会调用updateComponent方法更新组件。因为应用挂载时,第一次patch过程旧节点是空的,组件也没有被keep-alive,所以会继续执行mountComponent方法。

mountComponent

mountComponent接收参数和processComponent类似,只不过mountComponent参数中没有旧节点,只有initialVNode待被初始化的节点,即新节点。

mountComponent完整代码
ts
const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

  if (__DEV__ && instance.type.__hmrId) {
    registerHMR(instance)
  }

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

  if (isKeepAlive(initialVNode)) {
    ;(instance.ctx as KeepAliveContext).renderer = internals
  }

  // resolve props and slots for setup context
  if (!(__COMPAT__ && compatMountInstance)) {
    if (__DEV__) {
      startMeasure(instance, `init`)
    }
    setupComponent(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`)
  }
}

在挂载组件过程中,第一步就是创建组件实例:

ts
const compatMountInstance =
  __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
  compatMountInstance ||
  (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ))

创建完组件实例后,会针对KeepAlivevnode进行一些特殊化处理,即为instance.ctx添加一个renderer。这里忽略一些仅在开发环境下生效的代码。

ts
if (isKeepAlive(initialVNode)) {
  ;(instance.ctx as KeepAliveContext).renderer = internals
}

然后会调用一个setupComponent关键函数,该函数作用是在做一些组件初始化的工作,包括propsslots等的初始化、执行setup函数、options的初始化。

ts
if (!(__COMPAT__ && compatMountInstance)) {
  if (__DEV__) {
    startMeasure(instance, `init`)
  }
  setupComponent(instance)
  if (__DEV__) {
    endMeasure(instance, `init`)
  }
}

关于组件实例的创建过程及setupComponent的执行可以参考:组件实例的创建过程

接着,会处理Suspense。如果存在parentSuspense,异步setup的返回值会作为依赖注册到parentSuspense中。

ts
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
  parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

  // 如果initialVNode.el不为空创建一个占位符
  // TODO handle self-defined fallback
  if (!initialVNode.el) {
    const placeholder = (instance.subTree = createVNode(Comment))
    processCommentNode(null, placeholder, container!, anchor)
  }
  return
}

然后调用一个setupRenderEffect函数。

ts
setupRenderEffect(
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
)

setupRenderEffect

setupRenderEffect完整代码
ts
const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      let vnodeHook: VNodeHook | null | undefined
      const { el, props } = initialVNode
      const { bm, m, parent } = instance
      const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

      toggleRecurse(instance, false)
      // beforeMount hook
      if (bm) {
        invokeArrayFns(bm)
      }
      // onVnodeBeforeMount
      if (
        !isAsyncWrapperVNode &&
        (vnodeHook = props && props.onVnodeBeforeMount)
      ) {
        invokeVNodeHook(vnodeHook, parent, initialVNode)
      }
      if (
        __COMPAT__ &&
        isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
      ) {
        instance.emit('hook:beforeMount')
      }
      toggleRecurse(instance, true)

      if (el && hydrateNode) {
        // vnode has adopted host node - perform hydration instead of mount.
        const hydrateSubTree = () => {
          if (__DEV__) {
            startMeasure(instance, `render`)
          }
          instance.subTree = renderComponentRoot(instance)
          if (__DEV__) {
            endMeasure(instance, `render`)
          }
          if (__DEV__) {
            startMeasure(instance, `hydrate`)
          }
          hydrateNode!(
            el as Node,
            instance.subTree,
            instance,
            parentSuspense,
            null
          )
          if (__DEV__) {
            endMeasure(instance, `hydrate`)
          }
        }

        if (isAsyncWrapperVNode) {
          ;(initialVNode.type as ComponentOptions).__asyncLoader!().then(
            // note: we are moving the render call into an async callback,
            // which means it won't track dependencies - but it's ok because
            // a server-rendered async wrapper is already in resolved state
            // and it will never need to change.
            () => !instance.isUnmounted && hydrateSubTree()
          )
        } else {
          hydrateSubTree()
        }
      } else {
        if (__DEV__) {
          startMeasure(instance, `render`)
        }
        const subTree = (instance.subTree = renderComponentRoot(instance))
        if (__DEV__) {
          endMeasure(instance, `render`)
        }
        if (__DEV__) {
          startMeasure(instance, `patch`)
        }
        patch(
          null,
          subTree,
          container,
          anchor,
          instance,
          parentSuspense,
          isSVG
        )
        if (__DEV__) {
          endMeasure(instance, `patch`)
        }
        initialVNode.el = subTree.el
      }
      // mounted hook
      if (m) {
        queuePostRenderEffect(m, parentSuspense)
      }
      // onVnodeMounted
      if (
        !isAsyncWrapperVNode &&
        (vnodeHook = props && props.onVnodeMounted)
      ) {
        const scopedInitialVNode = initialVNode
        queuePostRenderEffect(
          () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
          parentSuspense
        )
      }
      if (
        __COMPAT__ &&
        isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
      ) {
        queuePostRenderEffect(
          () => instance.emit('hook:mounted'),
          parentSuspense
        )
      }

      // activated hook for keep-alive roots.
      // #1742 activated hook must be accessed after first render
      // since the hook may be injected by a child keep-alive
      if (
        initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
        (parent &&
          isAsyncWrapper(parent.vnode) &&
          parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
      ) {
        instance.a && queuePostRenderEffect(instance.a, parentSuspense)
        if (
          __COMPAT__ &&
          isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
        ) {
          queuePostRenderEffect(
            () => instance.emit('hook:activated'),
            parentSuspense
          )
        }
      }
      instance.isMounted = true

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        devtoolsComponentAdded(instance)
      }

      // #2458: deference mount-only object parameters to prevent memleaks
      initialVNode = container = anchor = null as any
    } else {
      // updateComponent
      // This is triggered by mutation of component's own state (next: null)
      // OR parent calling processComponent (next: VNode)
      let { next, bu, u, parent, vnode } = instance
      let originNext = next
      let vnodeHook: VNodeHook | null | undefined
      if (__DEV__) {
        pushWarningContext(next || instance.vnode)
      }

      // Disallow component effect recursion during pre-lifecycle hooks.
      toggleRecurse(instance, false)
      if (next) {
        next.el = vnode.el
        updateComponentPreRender(instance, next, optimized)
      } else {
        next = vnode
      }

      // beforeUpdate hook
      if (bu) {
        invokeArrayFns(bu)
      }
      // onVnodeBeforeUpdate
      if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
        invokeVNodeHook(vnodeHook, parent, next, vnode)
      }
      if (
        __COMPAT__ &&
        isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
      ) {
        instance.emit('hook:beforeUpdate')
      }
      toggleRecurse(instance, true)

      // render
      if (__DEV__) {
        startMeasure(instance, `render`)
      }
      const nextTree = renderComponentRoot(instance)
      if (__DEV__) {
        endMeasure(instance, `render`)
      }
      const prevTree = instance.subTree
      instance.subTree = nextTree

      if (__DEV__) {
        startMeasure(instance, `patch`)
      }
      patch(
        prevTree,
        nextTree,
        // parent may have changed if it's in a teleport
        hostParentNode(prevTree.el!)!,
        // anchor may have changed if it's in a fragment
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        isSVG
      )
      if (__DEV__) {
        endMeasure(instance, `patch`)
      }
      next.el = nextTree.el
      if (originNext === null) {
        // self-triggered update. In case of HOC, update parent component
        // vnode el. HOC is indicated by parent instance's subTree pointing
        // to child component's vnode
        updateHOCHostEl(instance, nextTree.el)
      }
      // updated hook
      if (u) {
        queuePostRenderEffect(u, parentSuspense)
      }
      // onVnodeUpdated
      if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
        queuePostRenderEffect(
          () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
          parentSuspense
        )
      }
      if (
        __COMPAT__ &&
        isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
      ) {
        queuePostRenderEffect(
          () => instance.emit('hook:updated'),
          parentSuspense
        )
      }

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        devtoolsComponentUpdated(instance)
      }

      if (__DEV__) {
        popWarningContext()
      }
    }
  }

  // create reactive effect for rendering
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))

  const update: SchedulerJob = (instance.update = () => effect.run())
  update.id = instance.uid
  // allowRecurse
  // #1801, #2043 component render effects should allow recursive updates
  toggleRecurse(instance, true)

  if (__DEV__) {
    effect.onTrack = instance.rtc
      ? e => invokeArrayFns(instance.rtc!, e)
      : void 0
    effect.onTrigger = instance.rtg
      ? e => invokeArrayFns(instance.rtg!, e)
      : void 0
    update.ownerInstance = instance
  }

  update()
}

setupRenderEffect看似很长,但将componentUpdateFn折叠起来,逻辑就清晰多了。

ts
const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 组件更新的副作用函数
  const componentUpdateFn = () => {
    // ...
  }

  // 创建一个关于渲染的ReactiveEffect
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update), // 一个调度器,依赖被触发时会执行(将updata加入到queue队列中)
    instance.scope // 在组件作用域内进行依赖追踪
  ))

  // 一个更新函数,这个更新函数中会执行effect.run方法
  const update: SchedulerJob = (instance.update = () => effect.run())
  update.id = instance.uid
  
  // 组件渲染允许递归更新
  toggleRecurse(instance, true)

  if (__DEV__) {
    effect.onTrack = instance.rtc
      ? e => invokeArrayFns(instance.rtc!, e)
      : void 0
    effect.onTrigger = instance.rtg
      ? e => invokeArrayFns(instance.rtg!, e)
      : void 0
    update.ownerInstance = instance
  }

  // 手动执行更新函数
  update()
}

setupRenderEffect主要,使用组件更新函数创建一个ReactiveEffect对象,然后声明一个update函数,并手动调用update函数。

由于update函数中执行了effect.run(),我们知道ReactiveEffect的实例方法run最终会调用副作用函数,以进行依赖的收集。所以继续执行componentUpdateFn函数。

componentUpdateFn

componentUpdateFn中分了两个分支:instance未挂载及instance已经挂载。

因为此时instance还未挂载,所以进入instance未挂载分支,进行挂载组件。

接下来,我们详细看下组件时如何进行挂载的。

首先声明一些变量:

ts
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
// bm:beforeMount钩子
// m:mounted钩子
// parent父组件实例
const { bm, m, parent } = instance
// 是否为AsyncComponentWrapper,通过defineAsyncComponent定义的组件会被AsyncComponentWrapper包裹
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

toggleRecurse(instance, false)

然后执行beforeMountonVnodeBeforeMount钩子:

ts
// beforeMount钩子
if (bm) {
  invokeArrayFns(bm)
}
// onVnodeBeforeMount钩子
if (
  !isAsyncWrapperVNode &&
  (vnodeHook = props && props.onVnodeBeforeMount)
) {
  invokeVNodeHook(vnodeHook, parent, initialVNode)
}
// vue2中以hook:beforeMount方式添加的钩子函数
if (
  __COMPAT__ &&
  isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
  instance.emit('hook:beforeMount')
}
toggleRecurse(instance, true)

接着会执行renderComponentRoot函数,获取当前组件的子虚拟dom树,并挂载子树。

ts
if (el && hydrateNode) {
  // ...
} else {
  if (__DEV__) {
    startMeasure(instance, `render`)
  }
  // 获取实例的子节点
  const subTree = (instance.subTree = renderComponentRoot(instance))
  if (__DEV__) {
    endMeasure(instance, `render`)
  }
  if (__DEV__) {
    startMeasure(instance, `patch`)
  }
  // 递归挂载subTree
  patch(
    null,
    subTree,
    container,
    anchor,
    instance,
    parentSuspense,
    isSVG
  )
  if (__DEV__) {
    endMeasure(instance, `patch`)
  }
  initialVNode.el = subTree.el
}
renderComponentRoot完整代码
ts
export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  const {
    type: Component,
    vnode,
    proxy,
    withProxy,
    props,
    propsOptions: [propsOptions],
    slots,
    attrs,
    emit,
    render,
    renderCache,
    data,
    setupState,
    ctx,
    inheritAttrs
  } = instance

  let result
  let fallthroughAttrs
  // 设置当前正在渲染的实例
  const prev = setCurrentRenderingInstance(instance)
  if (__DEV__) {
    accessedAttrs = false
  }

  // 执行render函数
  try {
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 有状态组件
      const proxyToUse = withProxy || proxy
      // 执行render函数
      result = normalizeVNode(
        render!.call(
          proxyToUse,
          proxyToUse!,
          renderCache,
          props,
          setupState,
          data,
          ctx
        )
      )
      fallthroughAttrs = attrs
    } else { // 函数式组件
      const render = Component as FunctionalComponent
      // in dev, mark attrs accessed if optional props (attrs === props)
      if (__DEV__ && attrs === props) {
        markAttrsAccessed()
      }
      // 执行render函数
      result = normalizeVNode(
        render.length > 1
          ? render(
              props,
              __DEV__
                ? {
                    get attrs() {
                      markAttrsAccessed()
                      return attrs
                    },
                    slots,
                    emit
                  }
                : { attrs, slots, emit }
            )
          : render(props, null as any /* we know it doesn't need it */)
      )
      // 如果函数式组件定义了props,fallthroughAttrs就是attrs,否则fallthroughAttrs中只包含class、style及on开头的属性
      fallthroughAttrs = Component.props
        ? attrs
        : getFunctionalFallthrough(attrs)
    }
  } catch (err) {
    blockStack.length = 0
    handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
    result = createVNode(Comment)
  }

  // 合并attr
  // in dev mode, comments are preserved, and it's possible for a template
  // to have comments along side the root element which makes it a fragment
  let root = result
  let setRoot: SetRootFn = undefined
  if (
    __DEV__ &&
    result.patchFlag > 0 &&
    result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
  ) {
    ;[root, setRoot] = getChildRoot(result)
  }
  
  // 允许透传attr
  if (fallthroughAttrs && inheritAttrs !== false) {
    const keys = Object.keys(fallthroughAttrs)
    const { shapeFlag } = root
    if (keys.length) {
      if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) { // root是普通HTML元素或组件
        if (propsOptions && keys.some(isModelListener)) { // keys中存在onUpdate:开头的属性
          // 该操作会保留fallthroughAttrs中非onUpdate:开头的属性及以onUpdate:开头但不在propsOptions中的属性
          fallthroughAttrs = filterModelListeners(
            fallthroughAttrs,
            propsOptions
          )
        }
        // 复制root,目的是合并root.props与fallthroughAttrs
        root = cloneVNode(root, fallthroughAttrs)
      } else if (__DEV__ && !accessedAttrs && root.type !== Comment) { // 其他情况,如果root不是注释,attrs不会被继承
        const allAttrs = Object.keys(attrs)
        const eventAttrs: string[] = []
        const extraAttrs: string[] = []
        for (let i = 0, l = allAttrs.length; i < l; i++) {
          const key = allAttrs[i]
          if (isOn(key)) {
            // ignore v-model handlers when they fail to fallthrough
            if (!isModelListener(key)) {
              // remove `on`, lowercase first letter to reflect event casing
              // accurately
              eventAttrs.push(key[2].toLowerCase() + key.slice(3))
            }
          } else {
            extraAttrs.push(key)
          }
        }
        if (extraAttrs.length) {
          warn(
            `Extraneous non-props attributes (` +
              `${extraAttrs.join(', ')}) ` +
              `were passed to component but could not be automatically inherited ` +
              `because component renders fragment or text root nodes.`
          )
        }
        if (eventAttrs.length) {
          warn(
            `Extraneous non-emits event listeners (` +
              `${eventAttrs.join(', ')}) ` +
              `were passed to component but could not be automatically inherited ` +
              `because component renders fragment or text root nodes. ` +
              `If the listener is intended to be a component custom event listener only, ` +
              `declare it using the "emits" option.`
          )
        }
      }
    }
  }

  // 兼容模式下开启INSTANCE_ATTRS_CLASS_STYLE,会将style与class添加到root.props中
  if (
    __COMPAT__ &&
    isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance) &&
    vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&
    root.shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)
  ) {
    const { class: cls, style } = vnode.props || {}
    if (cls || style) {
      if (__DEV__ && inheritAttrs === false) {
        warnDeprecation(
          DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE,
          instance,
          getComponentName(instance.type)
        )
      }
      // 将class与style添加到root.props中
      root = cloneVNode(root, {
        class: cls,
        style: style
      })
    }
  }

  // 继承指令
  if (vnode.dirs) {
    if (__DEV__ && !isElementRoot(root)) {
      warn(
        `Runtime directive used on component with non-element root node. ` +
          `The directives will not function as intended.`
      )
    }
    // 克隆root,因为root可能是个提升的节点
    root = cloneVNode(root)
    // 添加指令
    root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
  }
  // 继承transition
  if (vnode.transition) {
    if (__DEV__ && !isElementRoot(root)) {
      warn(
        `Component inside <Transition> renders non-element root node ` +
          `that cannot be animated.`
      )
    }
    root.transition = vnode.transition
  }

  if (__DEV__ && setRoot) {
    setRoot(root)
  } else {
    result = root
  }

  // 设置当前渲染中的实例
  setCurrentRenderingInstance(prev)
  // 返回根节点
  return result
}

renderComponentRoot函数中最重要的就是执行instancerender方法,生成instance的子vnode树。这个过程还会处理透传 Attribute

示例

下面我们以一个例子来理解应用挂载的流程:

html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.prod.js"
    }
  }
</script>

<div id="app"></div>

<script type="module">
  import { createApp, h, defineComponent, ref } from 'vue'
  
  const ComA = defineComponent({
    setup() {
      return () => h('span', 'ComA')
    }
  })
  
  createApp({
    setup() {
      return () => h('div', [ 'parent text', h(ComA) ])
    }
  }).mount('#app')
</script>
  • 首先使用createApp创建app示例,并调用其mount方法进行加载。

  • app.mount方法中,因为此时app还未进行加载,所以调用render函数进行渲染。在调用render函数前会先生成根vnode

    注意此时根vnodetypeObject,及它的shapeFlag4(即ShapeFlags.STATEFUL_COMPONENT),这决定了在patch过程进入哪个分支。

  • 创建完vnode后,执行render方法,在render方法中会执行patch方法。

    • patch方法中,根据vnode.typevnode.shapeFlag属性,进入shapeFlag & ShapeFlags.COMPONENT分支,执行processComponent方法。

    • processComponent中,由于n1null,继续进入mountComponent方法。

    • 进入mountComponent方法中就是组件正式挂载的流程了。其中首先就是根据vnode创建组件实例,然后调用setupComponent函数执行setup函数及options的初始化等操作。

    • 执行完setupComponent函数后,会执行setupRenderEffect。在setupRenderEffect声明组件的渲染函数componentUpdateFn,并创建一个ReactiveEffect实例和一个update更新函数。紧跟着调用update函数,在update函数中执行effect.run,在effect.run中会执行副作用函数(即组件渲染函数),继续调用组件渲染函数componentUpdateFn

    • componentUpdateFn中进入!instance.isMounted分支,调用renderComponentRoot函数生成当前组件实例的子vnode树。

      • renderComponentRoot中执行render函数,生成vnode树。注意此时vnodetypedivshapFlag17ShapeFlag.ELEMENT | ShapeFlag.ARRAY_CHILDREN)。此时没有透传attr需要处理,所以透传attr过程就跳过了。

    • 此时根组件的子vdomsubTree已经生成,接着调用patch方法挂载subTree

    • 这次进入patch,根据vnodetypeshapeFlag属性,进入shapeFlag & ShapeFlags.ELEMENT分支,执行processElement

      • processElement中,由于n1null,所以执行mountElement

      • 进入mountElement中,vnode.elnull,执行hostCreateElement函数创建DOMdiv)。

      • 接着,因为vnode.shapeFlag1717 & 16 !== 0shapeFlag & ShapeFlags.ARRAY_CHILDREN),所以会继续执行mountChildren方法挂载子节点。

      • mountChildren方法会遍历children,并对每个孩子节点执行patch方法。

      • 此时children中有两个节点。第一个节点为一个typeTextvnode(在mountChildren中调用patch前会对vnode进行标准化。在标准化的过程中会将字符串转为typeTextvnode),在patch过程中会调用processText,最终将字符串插入到div中;第二个节点为一个typeObjectvnode,它的挂载过程和根组件挂载过程类似,这里就不详细说明了,其最终结果就是渲染出的span标签插入到div标签中。

      • 执行hostInsertdiv插入div#app中。

  • render执行完毕, 调用flushPostFlushCbs,执行一些mounted钩子或watch等操作。

processText

processText方法用来处理静态文本节点。该类节点对应vdomtypeText(一个Symbol对象)。

例如以下模板经过编译后,text所对应的vdomtype就是Text。那么它在patch的过程就会进入processText中。SFC Playground

vue
<template>
  <Component>text</Component>
</template>

processText源码:

ts
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  if (n1 == null) { // n1为null,意味着这是个挂载操作
    // 在浏览器环境中hostInsert利用insertBefore方法进行添加子节点
    hostInsert(
      (n2.el = hostCreateText(n2.children as string)),
      container,
      anchor // 锚点,新创建的text节点被添加到该节点的前面
    )
  } else { // n1不为null,代表这是个更新操作
    const el = (n2.el = n1.el!)
    if (n2.children !== n1.children) { // 新旧节点的chidren不同时才会更新
      hostSetText(el, n2.children as string)
    }
  }
}

processCommentNode

processCommentNode方法用来处理注释节点。该类节点对应vdomtypeComment(一个Symbol对象)。使用createCommentVNode方法可以创建一个注释节点。

以下模板经过编译后,会使用createCommentVNode创建一个注释节点。SFC Playground

vue
<template>
  <!-- Comment -->
</template>

processCommentNode源码:

ts
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类型的vnode处理

Static类型的vnode所代表的并不一定是一个DOM节点,而是表示一个至多个连续静态DOM节点。对于Static类型的vnode,会直接进行批量插入。

vue
<template>
	<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    <li>10</li>
  </ul>
</template>

上方模板经过编译后,你会发现ul中的li会使用createStaticVNode创建一个Static类型的vnodeSFC Playground

patch中对于Static类型的vnode的处理:

ts
if (n1 == null) {
  mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
  patchStaticNode(n1, n2, container, isSVG)
}

挂载Static类型vnode

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.
  ;[n2.el, n2.anchor] = hostInsertStaticContent!(
    n2.children as string,
    container,
    anchor,
    isSVG,
    n2.el,
    n2.anchor
  )
}

processFragment

processFragment用来处理Fragment类型的vnodevue3template支持多个根组件,对于这多个根组件会使用一个Fragment类型的vnode进行表示。

如下面模板经过编译后,组件的根组件就是个Fragment类型的vnodeSFC Playground

ts
<template>
	<div>1</div>
  <div>2</div>
</template>

Fragment类型vnode的处理:

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
) => {
  // fragment的开始结束锚点
  const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
  const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

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

  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) { // 挂载
    // 插入fragment的开始结束锚点
    hostInsert(fragmentStartAnchor, container, anchor)
    hostInsert(fragmentEndAnchor, container, anchor)
    // 挂载子节点
    mountChildren(
      n2.children as VNodeArrayChildren,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    // ...
  }
}

processElement

processElement用来处理原生HTML节点。

ts
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) {
    mountElement(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

mountElement源码:

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
  ) { 
    // 如果vnode存在el属性,意味着vnode被重用了。
    // 如果vnode是静态节点,我们可以通过拷贝vnode.el,重用vnode.el
    el = vnode.el = hostCloneNode(vnode.el)
  } else { // 否则根据vnode创建DOM,并将DOM添加到vnode.el中
    el = vnode.el = hostCreateElement(
      vnode.type as string,
      isSVG,
      props && props.is,
      props
    )

    // 先挂载children,因为某些props可能依赖孩子节点,如<select value>
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 子节点是文本,直接创建文本DOM
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 自己节点是数组,调用mountChildren挂载子节点
      mountChildren(
        vnode.children as VNodeArrayChildren,
        el,
        null,
        parentComponent,
        parentSuspense,
        isSVG && type !== 'foreignObject',
        slotScopeIds,
        optimized
      )
    }
    // 执行vnode中的所有指令的created钩子
    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'created')
    }
    // 为DOM添加props
    if (props) {
      for (const key in props) {
        if (key !== 'value' && !isReservedProp(key)) { // key不是value,也不是vue中保留的props,如空字符串、ref、key等
          hostPatchProp(
            el,
            key,
            null,
            props[key],
            isSVG,
            vnode.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
      // 一些属性应该在value之前被设置,如min/max
      if ('value' in props) {
        hostPatchProp(el, 'value', null, props.value)
      }
      // vnode挂载前钩子
      if ((vnodeHook = props.onVnodeBeforeMount)) {
        invokeVNodeHook(vnodeHook, parentComponent, vnode)
      }
    }
    // scopeId
    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
    })
  }
  // 执行vnode中的所有指令的beforeMount钩子
  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
  // 执行transition的beforeEnter钩子
  const needCallTransitionHooks =
    (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
    transition &&
    !transition.persisted
  if (needCallTransitionHooks) {
    transition!.beforeEnter(el)
  }
  // 插入el
  hostInsert(el, container, anchor)
  // 有需要执行的vnodeMounted钩子或transition.enter钩子或指令的mounted钩子时
  // 将这些钩子的执行放入pendingPostFlushCbs队列中,等到DOM更新后执行
  if (
    (vnodeHook = props && props.onVnodeMounted) ||
    needCallTransitionHooks ||
    dirs
  ) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
      needCallTransitionHooks && transition!.enter(el)
      dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
    }, parentSuspense)
  }
}

挂载Teleport

Teleport组件在被转为vnode后,其shapeFlagShapeFlags.TELEPORT。对于Teleport的挂载,处理如下:

ts
export const TeleportImpl = {
  __isTeleport: true,
  process(
    n1: TeleportVNode | null,
    n2: TeleportVNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean,
    internals: RendererInternals
  ) {
    const {
      mc: mountChildren,
      pc: patchChildren,
      pbc: patchBlockChildren,
      o: { insert, querySelector, createText, createComment }
    } = internals

    // 是否禁用Teleport
    const disabled = isTeleportDisabled(n2.props)
    let { shapeFlag, children, dynamicChildren } = n2

    if (__DEV__ && isHmrUpdating) {
      optimized = false
      dynamicChildren = null
    }

    if (n1 == null) { // 挂载Teleport
      // teleport DOM位置(行内位置):
      // <div>placeholder | teleport | mainAnchor</div>
      // target中的位置
      // <target>teleport | targetAnchor</target>
      
      // teleport开始的位置
      const placeholder = (n2.el = __DEV__
        ? createComment('teleport start')
        : createText(''))
      // teleport结束的位置
      const mainAnchor = (n2.anchor = __DEV__
        ? createComment('teleport end')
        : createText(''))
      // 将placeholder、mainAnchor先后插入到container中
      insert(placeholder, container, anchor)
      insert(mainAnchor, container, anchor)
      // 需要挂载的到的目标
      const target = (n2.target = resolveTarget(n2.props, querySelector))
      // 挂载目标的锚点,被挂载的内容会被挂载在锚点前面
      const targetAnchor = (n2.targetAnchor = createText(''))
      if (target) {
        // 向目标中插入锚点
        insert(targetAnchor, target)
        // #2652 we could be teleporting from a non-SVG tree into an SVG tree
        isSVG = isSVG || isTargetSVG(target)
      } else if (__DEV__ && !disabled) {
        warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
      }

      const mount = (container: RendererElement, anchor: RendererNode) => {
        // Teleport总是具有数组孩子,所有调用mountChildren进行挂载
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(
            children as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }
      }
      
      // 如果禁用了teleport,内容会被挂载到container中
      // 否则挂载到target中
      if (disabled) {
        mount(container, mainAnchor)
      } else if (target) {
        mount(target, targetAnchor)
      }
    } else {
      // ...
    }
  },
  
  // ...
}

挂载Suspence

Suspense组件在被转为vnode后,其shapeFlagShapeFlags.SUSPENSE。对于Suspense的挂载,处理如下:

ts
export const SuspenseImpl = {
  name: 'Suspense',
  __isSuspense: true,
  process(
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean,
    // platform-specific impl passed from renderer
    rendererInternals: RendererInternals
  ) {
    if (n1 == null) {
      mountSuspense(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized,
        rendererInternals
      )
    } else {
      patchSuspense(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        isSVG,
        slotScopeIds,
        optimized,
        rendererInternals
      )
    }
  },
  
  // ...
}

Suspence的挂载通过mountSuspense函数进行:

ts
function mountSuspense(
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean,
  rendererInternals: RendererInternals
) {
  const {
    p: patch,
    o: { createElement }
  } = rendererInternals
  // 一个暂时存放suspense内容的div
  const hiddenContainer = createElement('div')
  // 创建suspense的边界
  const suspense = (vnode.suspense = createSuspenseBoundary(
    vnode,
    parentSuspense,
    parentComponent,
    container,
    hiddenContainer,
    anchor,
    isSVG,
    slotScopeIds,
    optimized,
    rendererInternals
  ))

  // 挂载suspense中default插槽对应的vnode。注意这里挂载到的是hiddenContainer
  patch(
    null,
    (suspense.pendingBranch = vnode.ssContent!),
    hiddenContainer,
    null,
    parentComponent,
    suspense,
    isSVG,
    slotScopeIds
  )
  // suspense存在异步依赖
  if (suspense.deps > 0) {
    // 触发pending、fallback事件
    triggerEvent(vnode, 'onPending')
    triggerEvent(vnode, 'onFallback')

    // 挂载 fallback vnode,这里直接挂载到container上了
    patch(
      null,
      vnode.ssFallback!,
      container,
      anchor,
      parentComponent,
      null, // fallback tree will not have suspense context
      isSVG,
      slotScopeIds
    )
    // 设置suspense中当前被激活的分支
    setActiveBranch(suspense, vnode.ssFallback!)
  } else {
    // Suspense没有异步依赖,只需要解析即可
    suspense.resolve()
  }
}

function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
  // 指定当前被激活的分支
  suspense.activeBranch = branch
  const { vnode, parentComponent } = suspense
  // suspense对应vnode的el指向当前激活分支对应的el
  const el = (vnode.el = branch.el)
  // 如果suspense是组件的根节点,递归更新HOC el
  if (parentComponent && parentComponent.subTree === vnode) {
    parentComponent.vnode.el = el
    updateHOCHostEl(parentComponent, el)
  }
}

总结

应用实例的挂载流程:

应用的挂载 has loaded