1

当我们像下面这样使用 createApp 创建 vue app 实例过程中发生了什么?

const { createApp } = Vue
createApp({
    setup() {
      return {
      }
    }
})
.mount('#app')

我们一起来看看。


首先, 进入的是 vue导出的 createApp 函数, 它将所有参数都合并为了 args, 并调用了 ensureRenderer 函数并调用了其返回数据上的createApp , 然后将 args 打散传入 。

这里可以看出 ensureRenderer 调用的是 runtime-core 内导出的 createRenderer 方法, createRenderer 接收了 rendererOptions ,

这样做的目的是,一是使用renderer 做了单例缓存, 避免创建多个renderer, 二是vue3 考虑到更好的支持多端的渲染,没有强耦合浏览器的 dom 操作, 而是把一些操作的具体实现暴露给开发者自己去实现, 这里传入的 rendererOptions 就是 浏览器 dom 环境的具体实现


import {
  createRenderer
} from '@vue/runtime-core'

const rendererOptions = extend({ patchProp }, nodeOps) // 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法

let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

function ensureRenderer() { 
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

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

  //...
})

createRenderer 方法内部调用的又是 baseCreateRenderer

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

baseCreateRenderer 内部内容就很多了,这里的options 就是外部传入的平台相关的操作细节 rendererOptions ,然后将 insert、remove 等方法从options 中取出。 在下方实现的渲染相关的一系列方法中调用, 因为内容过长,这里将其省略了。 再往下, 函数的末尾呢导出了二个比较重要的东西, render 方法, createApp 方法, 然后 createApp 方法又是通过 createAppAPI 方法创建的的, 其接受了 render 和 hydrate 。 这也是外部可以调用 ensureRenderer().createApp(...args) 的原因

import { createAppAPI } from './apiCreateApp'

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {


  const { 
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options  
  
  const patch: PatchFn = () => {/*.... */ }
  const processText: ProcessTextOrCommentFn = = () => {/*.... */ }
  const processCommentNode: ProcessTextOrCommentFn = = () => {/*.... */ }
  const mountStaticNode = = () => {/*.... */ }
  const patchStaticNode = () => {/*.... */ }
  const moveStaticNode = () => {/*.... */ }
  const removeStaticNode = () => {/*.... */ }
  const processElement = () => {/*.... */ }
  const mountElement = () => {/*.... */ }
  const setScopeId = () => {/*.... */ }
  const mountChildren: MountChildrenFn = () => {/*.... */ }
  const patchElement = () => {/*.... */ }
  // The fast path for blocks.
  const patchBlockChildren: PatchBlockChildrenFn = () => {/*.... */ }
  const patchProps = () => {/*.... */ }
  const processFragment = () => {/*.... */ }
  const processComponent = () => {/*.... */ }
  const mountComponent: MountComponentFn = () => {/*.... */ }
  const updateComponent = () => {/*.... */ }
  const setupRenderEffect: SetupRenderEffectFn = () => {/*.... */ }
  const updateComponentPreRender = () => {/*.... */ }
  const patchUnkeyedChildren = () => {/*.... */ }
  const patchKeyedChildren = () => {/*.... */ }
  const move: MoveFn = () => {/*.... */ }
  const unmount: UnmountFn = () => {/*.... */ }
  const remove: RemoveFn = () => {/*.... */ }
  const removeFragment = () => {/*.... */ }
  const unmountComponent = () => {/*.... */ }
  const unmountChildren: UnmountChildrenFn = () => {/*.... */ }
  const getNextHostNode: NextFn = () => {/*.... */ }
  const render: RootRenderFunction = () => {/*.... */ }
  const internals: RendererInternals = () => {/*.... */ }


  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

createAppAPI 内部返回了 createApp 函数。 这个函数内部主要是创建了 app 对象基本数据结构,其上有 uuid, use(注册插件) , component (注册子组件), mount 等等属性和方法。 这里要注意的是, mount 方法内部调用了 createAppAPI 接收的 render 渲染器,此时以闭包形式存在, 最后返回了 app对象


export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, 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
      },

      use(plugin: Plugin, ...options: any[]) { 
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) { 
          installedPlugins.add(plugin)
          plugin.install(app, ...options) 
        } else if (isFunction(plugin)) { 
          installedPlugins.add(plugin)
          plugin(app, ...options)
        }
        return app
      },

      mixin(mixin: ComponentOptions) {
        if (__FEATURE_OPTIONS_API__) {
          if (!context.mixins.includes(mixin)) {
            context.mixins.push(mixin)
          }
        }
        return app
      },
      // 注册组件
      component(name: string, component?: Component): any {

        if (!component) { 
          return context.components[name]
        }

        context.components[name] = component
        return app
      },

      directive(name: string, directive?: Directive) {
        if (!directive) {
          return context.directives[name] as any
        }
        context.directives[name] = directive
        return app
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean, 
        isSVG?: boolean
      ): any {
        if (!isMounted) { 
          const vnode = createVNode( 
            rootComponent as ConcreteComponent,
            rootProps
          )
          vnode.appContext = context

          render(vnode, rootContainer, isSVG)
          isMounted = true
          app._container = rootContainer 
          ;(rootContainer as any).__vue_app__ = app  // app 挂到 dom __vue_app__ 属性上
          return vnode.component!.proxy
        }
      },

      unmount() {
        if (isMounted) {
          render(null, app._container)
          delete app._container.__vue_app__
        }
      },

      provide(key, value) {
        context.provides[key as string] = value
        return app
      }
    })
    return app
  }
}

返回 app后,这时就回到了一开始的 createApp 函数, 将返回的app实例存储在了 app变量内, 并取出 mount 方法,并重新定义了一个 app.mount 方法,对原本的mount 方法进行了一次包装, 包装的目的呢和 renderer 是一样的, 将平台的具体实现抽离,由开发者自行实现,
这里 app.mount 方法所接收的 containerOrSelector 参数就是我们外部传入的 #app 选择器 , 然后使用 normalizeContainer 对这个选择器进行标准化,返回的是实际的dom, 如果没找到实际的dom的话就直接return 了, 如果发现组件没有提供template 、render ,那就会取 dom 的 innerHTML作为 template , 并在调用mount方法挂载前清空实际dom的 innerHTML , 挂载完后移除了v-cloak 等属性,并将 app返回

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

  const { mount } = app // 取出原本的 mount 
  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 
    }
    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>

当外部调用mout 方法时, app.mount 自然就被调用了

createApp().mount('#app')

这就又进入到了在 createApp 中定义的最初的 mount , 内部创建了 vNode , 并调用了render 方法进行渲染, 并将 isMounted 修改

  mount( 
    rootContainer: HostElement,
    isHydrate?: boolean, 
    isSVG?: boolean
  ): any {
    if (!isMounted) {
      const vnode = createVNode(
        rootComponent as ConcreteComponent,
        rootProps
      )
      vnode.appContext = context

      render(vnode, rootContainer, isSVG)

      isMounted = true
      app._container = rootContainer 
          ;(rootContainer as any).__vue_app__ = app  
      return vnode.component!.proxy
    } 
  }

好了, 这样整个流程就走完了

整理一下,整个调用过程如下:

createApp -> ensureRenderer -> baseCreateRenderer -> createAppAPI (接收 render方法) -> createApp 创建并返回 app对象 -> 回到 createApp ,包装app.mount -> 外部调用 .mount('#app') -> 触发 app.mount -> mount -> 调用 render 方法渲染


nxl3477
299 声望15 粉丝

挑战10000小时定律。