[Vue3源码剖析(一)]App组件的创建和挂载过程

231 阅读6分钟

应用的创建过程

createApp:通过单步调试法可以看到createApp函数

export const createApp = ((...args) => {
  // 返回一个renderer(渲染器),渲染器中有createApp方法
  const app = ensureRenderer().createApp(...args)
  ...
  return app
}) as CreateAppFunction<Element>

baseCreateRendere:挨个进入函数后发现最后在baseCreateRendere(渲染器函数)中创建渲染器,该函数代码很长,直接拉到2300行

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

createAppAPI:可以发现app实例就在里面创建的,实例:{use(){}, component(){},....}

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }

    const context = createAppContext()
    const installedPlugins = new Set()

    let isMounted = false

    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {
        return context.config
      },

      set config(v) {...},

      use(plugin: Plugin, ...options: any[]) {...},

      mixin(mixin: ComponentOptions) {...},

      component(name: string, component?: Component): any {...},

      directive(name: string, directive?: Directive) {...},

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {...},

      unmount() {...},

      provide(key, value) {...)

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }
}

挂载过程

mount:会创建VNode传入render函数中

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          ) // 创建VNode

          vnode.appContext = context

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

          if (isHydrate && hydrate) { // isHydrate为null
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else { 
            render(vnode, rootContainer, isSVG) // 执行render
          }
          ...
      },

问题一:mount参数类型问题

我们可以看到mount(rootContainer: HostElement,...)接收的第一个参数的元素类型,但是平时在开发中,允许通过app.mount("#app")的方式传入字符串,那这是什么原因呢?

其实在createApp函数中对mount进行了重写,重写主要是考虑跨平台

export const createApp = ((...args) => {
  // 返回一个renderer
  const app = ensureRenderer().createApp(...args)

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

  const { mount } = app // 保存原先的mount方法
  // 对mount进行重写
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // normalizeContainer方法就是在web端获取我们的元素,比如div#app
    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中的原本内容
    container.innerHTML = ''
    // 调用真正的mount函数,进行挂载
    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>

render:由于在mount中创建了vnode,所以走patch函数,详细写在注释中了

  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {  // 如果vnode为null,进行卸载操作
      if (container._vnode) {
        unmount(container._vnode, null, null, true) // 调用unmount函数进行卸载操作
      }
    } else { // 如果vnode不为null,说明此时需要进行挂在或者更新(由于在mount中传递了vnode,所以走patch函数)
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    // 将新vnode赋值到container上,下次渲染的时候,container._vnode就是oldVnode,那么就会进行更新操作。
    container._vnode = vnode
  }

patch:根据虚拟dom的类型进行不同的处理(首次传入的是组件,所以执行的是processComponent函数)

 const patch: PatchFn = (
    n1, // n1是oldVNode
    n2, // n2是newVNode
    container, // 渲染后会将vnode渲染到conoiner上
    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
    }

    // 根据 n2 Vnode 的类型进行不同的处理
    const { type, ref, shapeFlag } = n2
    switch (type) {
      // 如果当前的 vnode 是文本节点的话,使用 processText 进行处理
      case Text:
        processText(n1, n2, container, anchor)
        break
      // 如果当前的 vnode 是注释节点的话,使用 processCommentNode 进行处理
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      // 如果当前的 vnode 是静态节点的话,调用 mountStaticNode 和 patchStaticNode 进行处理
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      // 如果节点是 Fragment 的话,使用 processFragment 进行处理
      // Fragment 节点的作用是使组件拥有多个根节点
      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) {...
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {...
        } else if (__DEV__) {...}
    }

processComponent:首次创建走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
    if (n1 == null) { // n1等于null,表示挂载节点(首次创建n1为null)
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else { 
        mountComponent( // 所以第一次进入mountComponent
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {// n1不为null,那么就更新组件
      updateComponent(n1, n2, optimized)
    }
  }

mountComponent:中进行如下三步

  1. createComponentInstance:创建instance实例,但是所有api都为null
  2. setupComponent:对组件实例初始化,进行操作和赋值的代码。在另外一篇文章中有详细讲解 [Vue3源码剖析(二)]组件实例创建过程
  3. setupRenderEffect:调用设置和渲染有副作用的函数
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const compatMountInstance =
      __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component

    // 1.调用createComponentInstance创建组件实例(创建出来的实例,实例中的属性都是null)
    const instance: ComponentInternalInstance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

        ...
        
      // 2.对组件实例的props/slot/data等进行初始化处理
      // 对所有数据进行操作和赋值的代码
      setupComponent(instance)
      if (__DEV__) {
        endMeasure(instance, `init`)
      }
    }

      ...

    // 3.调用设置和渲染有副作用的函数
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

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

setupRenderEffect:effect作用是当组件的数据发生变化时,effect函数包裹就会执行

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const componentUpdateFn = () => {
      // 组件没有挂载,就进行一个挂载操作
      if (!instance.isMounted) {
      
          ...
          
          // 调用renderComponentRoot渲染组件,生成子树的vnode
          // 因为template会被编译成render函数,所以renderComponentRoot就是去执行render函数获取对应的vnode
          const subTree = (instance.subTree = renderComponentRoot(instance))
          
          ...
          
          // 进行patch的递归过程
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
          
          ...
          
        instance.isMounted = true // 之后就走更新

        ...
      }
      // 更新组件
      else {...}

此时又回到了patch函数(patch函数在上面)中,在vue3中是template内允许有多个根,如果多个根就会执行processFragment,单个根就会执行processElement。实际上这两个函数的逻辑都差不多,以下就看processElement函数吧。

processElement:可以看出我们是第一次挂载那么执行mountElement

  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 {
    // 有旧node进行更新操作
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

mountElement:如果该元素没有子元素,只有文本节点就直接渲染文本。有子元素就进入mountChildren

  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, dirs } = vnode

    // 根据类型和其他属性,创建DOM元素节点
    // hostCreateElement是跨平台的函数,在web下会执行document.createElement函数
    // 所以最终不管是vue还是react,他们都会操作dom,只是对我们都是不可见的。
    el = vnode.el = hostCreateElement(
      vnode.type as string,
      isSVG,
      props && props.is,
      props
    )
      // 如果子节点是纯文本的情况
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 
      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
      )
    }
    ...
   }

mountChildren:会遍历所有的子节点,挨个进行patch

  const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized,
    start = 0
  ) => {
    // 对子节点进行遍历挨个进行patch
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      patch(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

总结

以下是挂载的整个流程

image.png