前情回顾

本篇文章对 app.mount(“#app”) 实现流程进行了详细分析,以该系列文章(一)中的 demo 为例。

戳链接回顾该系列文章(一)

Vue3源码学习笔记—— Vue.createApp(App) 和 app.mount(“#app”)(一) - 掘金 (juejin.cn)


戳链接回顾该系列文章(二)—— Vue.createApp(App) 做了什么

Vue3源码学习笔记—— Vue.createApp(App) 和 app.mount(“#app”)(二) - 掘金 (juejin.cn)

mount 实现流程

重写 mount 函数

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

createApp 函数中,首先取出 app 对象中的 mount 函数,然后通过 app.mount = () => {} 对 mount 函数进行重写:

① 首先调用 normalizeContainer 函数来获取container节点;

② 判断该节点是否存在,若不存在,则直接返回;

③ 清空container的innerHTML;

④ 调用mount函数。

export const createApp = ((...args) => {
  // 1.创建app对象
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }
  
  // 2.这里取出了app中的mount方法,因为下面要进行重写
  const { mount } = app
  
  // 3.重写mount方法
  /*  
    (1)这里重写的目的是考虑到跨平台(app.mount里面只包含和平台无关的代码)
    (2)这些重写的代码都是一些和web关系比较大的代码(比如其他平台也可以进行类似的重写)
  */
  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) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      component.template = container.innerHTML
      // 2.x compat check
      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
          }
        }
      }
    }

    // clear content before mounting
    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>

mount 函数

位置:/packages/runtime-core/src/apiCreateApp.ts

核心流程:① 根据传入的根组件App创建vnode;② 渲染vnode。

mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  if (!isMounted) {
    // #5571
    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.`
      )
    }
    // 1.创建根组件的vnode
    // 使用createVNode来创建vnode对象
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    // store app context on the root VNode.
    // this will be set on the root instance on initial mount.
    vnode.appContext = context
    // HMR root reload
    if (__DEV__) {
      context.reload = () => {
        (cloneVNode(vnode), rootContainer, isSVG)
      }
    }
    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      // 2.渲染vnode
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    // for devtools and telemetry
    ;(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)\``
    )
  }
}

render 函数

位置:/packages/runtime-core/src/renderer.ts

在 mount 函数中,通过调用 render 函数来实现渲染vnode,而 render 函数是 baseCreateRenderer 函数返回调用 createAppAPI 时传入的参数之一,也就是说,render 函数是在 baseCreateRenderer 函数中定义的。

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  ......
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    // 如果vnode为null,那么就会销毁组件
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 创建或者更新组件都是使用patch函数(这里就是将根组件挂载到DOM上)
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    // 放到container上面,缓存vnode
    container._vnode = vnode
  }
  
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

该函数中,由于我们在 mount 中创建了 vnode,vnode 存在,因此调用 patch 函数,而此时 container._vnode 是不存在的,所以相当于 patch(null, vnode, container)

patch 函数

位置:/packages/runtime-core/src/renderer.ts

patch 对传入的 vnode 进行类型的判断,由于我们一开始传入的参数为根组件 App,因此,接下来调用 processComponent 函数来处理组件。

const patch: PatchFn = (
  n1, // n1 表示旧的vnode,当n1为null时就表示是一次挂载(挂载or更新由n1决定)
  n2, // n2 表示新的vnode,根据n2的type进行不同的处理
  container, // 渲染后会将vnode渲染到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: ...   //处理文本节点
    case Comment: ...   //处理注释节点
    case Static: ...   //处理静态节点
    case Fragment: ...   //处理Fragment组件节点
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) { ... }   // 处理普通的DOM元素,比如div/button/span
      else if (shapeFlag & ShapeFlags.COMPONENT) {   // 处理组件节点
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
      else if (shapeFlag & ShapeFlags.TELEPORT) { ... } 
      else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ... } 
      else if (__DEV__) {
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }

  // set ref
  if (ref != null && parentComponent) { ... }
}

processComponent 函数

位置:/packages/runtime-core/src/renderer.ts

processComponent 函数对传入的 n1(旧的 vnode )进行判断,若 n1 为空,则挂载节点,若 n1 不为空,则更新组件。此处 n1 为空,因此接下来执行 mountComponent 函数挂载组件。

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
  // n1等于null,表示挂载节点
  if (n1 == null) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      ...
    } else {
      // 调用mountComponent挂载组件
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  } else { // n1不为null,表示更新组件
    updateComponent(n1, n2, optimized)
  }
}

mountComponent 函数

位置:/packages/runtime-core/src/renderer.ts

mountComponent 函数首先调用 ComponentInternalInstance() 创建组件的实例对象,该实例对象有很多属性,但都置空,而后通过调用 setupComponent(instance) 函数来对组件所有的数据进行操作和赋值。处理完组件实例对象的数据后,调用设置和渲染有副作用的函数 setupRenderEffect()

const mountComponent: MountComponentFn = (
  ...
) => {
  // 2.x compat may pre-create the component instance before actually
  // mounting
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
    
  // 1.调用ComponentInternalInstance创建组件的实例
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    // 调用createComponentInstance函数创建一个实例对象,其属性皆为没有值
    (initialVNode.component = createComponentInstance(
      ...
    ))
  ......
  // resolve props and slots for setup context
  if (!(__COMPAT__ && compatMountInstance)) {
    if (__DEV__) {
      startMeasure(instance, `init`)
    }
    // 2.setup组件实例,作用是对组件的props/slots/data等进行初始化处理
    setupComponent(instance)
    if (__DEV__) {
      endMeasure(instance, `init`)
    }
  }
  ......
  // 调用设置和渲染有副作用的函数
  setupRenderEffect(
    ...
  )
  if (__DEV__) {
    popWarningContext()
    endMeasure(instance, `mount`)
  }
}

SetupRenderEffectFn 函数

位置:/packages/runtime-core/src/renderer.ts

SetupRenderEffectFn 函数首先判断传入组件是否已被挂载,若没有被挂载,则挂载组件,并在挂载完把组件的 isMounted 属性设置为true,表示已挂载;若已被挂载,则更新组件。
此时我们传入的组件是 根组件 App,应该挂载组件,递归调用 patch 函数挂载组件。

  const setupRenderEffect: SetupRenderEffectFn = (
    ...
  ) => {
    const componentUpdateFn = () => {
      // 如果组件没有被挂载,那么挂载组件
      if (!instance.isMounted) {
        ......
        if (el && hydrateNode) {
          ......
        } else {
          ......
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
          if (__DEV__) {
            endMeasure(instance, `patch`)
          }
          initialVNode.el = subTree.el
        }
        ......
        instance.isMounted = true
        ......
      } else { // 更新组件
        ......
      }
    }
    .....
  }

在 vue2 中,一个 template 只能有一个根元素,即 template 中的结构为:

<template>
  <div>
    <h2>Oooorange</h2>
    <p>{{message}}</p>
    <button @click="changeMessage">修改message</button>
  </div>
</template>

而 vue3 中,一个 template 内是可以有多个根元素的,那是因为,当根元素有多个时,vue3会将多个根元素用 Fragment 元素包裹起来,则实际上 template 中的结构为:

<template>
  <Fragment>
    <h2>Oooorange</h2>
    <p>{{message}}</p>
    <button @click="changeMessage">修改message</button>
  </Fragment>
</template>

来到 patch 函数,需要通过判断传入的组件类型执行相应的操作,而 demo 中 template 内有多个根元素,因此此时传入的组件类型应为 Fragment ,因此接下来执行 processFragment 函数。

const patch: PatchFn = (
  ...
) => {
  ...
  switch (type) {
    ...
    case Fragment:  //处理Fragment组件节点
      processFragment( ... )
      break
    default: ...
  }
  ...
}

processFragment 函数

位置:/packages/runtime-core/src/renderer.ts

与上述 processComponent 函数执行思路相似,patch 函数判断类型后执行的函数都是这个思路,对传入的旧vnode进行判断并作出对应操作,即对传入的 n1(旧的 vnode )进行判断,若 n1 为空,则挂载节点,若 n1 不为空,则更新组件

processFragment 函数中,此处 n1 为空,因此接下来执行 mountChildren 函数。

  const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    ...
  ) => {
    ...
    if (n1 == null) {  // n1等于null,表示挂载节点
      ...
      mountChildren( ... )
    } else { // n1不为null,表示更新组件
      patchChildren( ... )
    }
  }

mountChildren 函数

位置:/packages/runtime-core/src/renderer.ts

mountChildren 函数遍历 children ,采用了深度遍历的思想挂载所有子元素。

const mountChildren: MountChildrenFn = (
  children,
  container,
  。。。
) => {
  // 遍历children,调用对应的patch方法
  for (let i = start; i < children.length; i++) {
    const child = (children[i] = optimized
      ? cloneIfMounted(children[i] as VNode)
      : normalizeVNode(children[i]))
      // 调用patch,如果继续有子节点就会一次执行(其实是一个深度优先的算法)
    patch(
      null,
      child,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

到这里,我们就已经进入普通的 DOM 元素的遍历了,进入 patch 函数后,将执行 processElement 函数处理普通的 DOM 元素。

const patch: PatchFn = (
  ...
) => {
  ...
  switch (type) {
    ...
    default: 
      if (shapeFlag & ShapeFlags.ELEMENT) {  // 处理普通的DOM元素,比如div/button/span
          processElement( ... )
        } ...
  }
  ...
}

processElement 函数

位置:/packages/runtime-core/src/renderer.ts

processElement 函数中,此处 n1 为空,因此接下来执行 mountElement 函数。

const processElement = ( ... 
) => {
  isSVG = isSVG || (n2.type as string) === 'svg'
  // 判断n1是否为null,为null表示要挂载节点
  if (n1 == null) {  // 调用mountElement挂载节点
    mountElement( ... )
  } else {  // 不为null表示要更新节点
    patchElement( ... )
  }
}

mountElement 函数

位置:/packages/runtime-core/src/renderer.ts

mountElement 函数核心流程:

① 根据类型和其他属性,创建DOM元素节点;

② 判断子节点的类型,若子节点为纯文本,则直接处理纯文本,若子节点为数组,则调用 mountChildren 函数深度遍历处理子节点;

③ 处理 props 属性;

④ 将el挂载到 container 中。

  const mountElement = (
    vnode: VNode,
    container: RendererElement,
    ...
  ) => {
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
    if ( ... ) { ...
    } else {
      // 1.根据类型和其他属性,创建DOM元素节点
      el = vnode.el = hostCreateElement(  // 相当于调用document.createElement()
        vnode.type as string,
        isSVG,
        props && props.is,
        props
      )
      
      // 2.如果子节点是纯文本的情况,则调用 hostSetElementText 函数处理纯文本
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(el, vnode.children as string)
      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {  // 3.如果子节点是一个数组的情况
        mountChildren( ... )
      }      
      ...
      // props
      // 4.处理props属性<div class="" style="">
      if (props) { ... }
      // scopeId
      setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
    }
    ...
    // 5.调用该方法将el挂载到container中
    hostInsert(el, container, anchor)
    ...
  }

总结

mount 实现流程总结

到这里,我们就执行完 demo 中组件的挂载了,下面我用一张流程图对整体流程进行总结。

在这里插入图片描述

Vue.createApp(App) 和 app.mount(“#app”) 总结

在这里插入图片描述

最后

Vue.createApp(App) 和 app.mount(“#app”) 的内部实现原理到此就告一段落啦,如有错误,欢迎指正;尚有不足,请多指教~~~

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐