Vue3源码系列(七):createApp— 一切的起源

317 阅读6分钟

前面几篇介绍的大都是reactivity相关的API。我们在使用Vue3作为前端框架时,往往在我们的main.js/main.ts里来创建vue3app实例,就会用到createApp这个API。本篇就来简要了解一下createApp里发生的故事。

一、相关 ts 类型

可以先瞄一眼与createAppApi相关的ts类型,这样就更能理解它的使用,这里挑几个简要介绍一下。

1. App

AppcreateApp返回值的类型,可以看到项目里常用的一些方法都在这里,某些方法返回了this,则可以链式调用。此外,还兼容了vue2filter,还有一些内部的属性。

export interface App<HostElement = any> {
  version: string
  // config 上有常用到的 globalProperties
  config: AppConfig
  use(plugin: Plugin, ...options: any[]): this
  mixin(mixin: ComponentOptions): this
  component(name: string): Component | undefined
  component(name: string, component: Component): this
  directive(name: string): Directive | undefined
  directive(name: string, directive: Directive): this
  mount(
    rootContainer: HostElement | string,
    isHydrate?: boolean,
    isSVG?: boolean
  ): ComponentPublicInstance
  unmount(): void
  provide<T>(key: InjectionKey<T> | string, value: T): this
​
  // internal, but we need to expose these for the server-renderer and devtools
  _uid: number
  _component: ConcreteComponent
  _props: Data | null
  _container: HostElement | null
  _context: AppContext
  _instance: ComponentInternalInstance | null
​
  /**
   * v2 compat only
   */
  filter?(name: string): Function | undefined
  filter?(name: string, filter: Function): this
​
  /**
   * @internal v3 compat only
   */
  _createRoot?(options: ComponentOptions): ComponentPublicInstance
}

2. AppConfig

创建的App的配置,包含的内容在vue2里基本都有,重要的例如组件合并策略optionMergeStrategiesVue全局属性globalProperties(Vue2里直接挂到原型上)、编译器选项compilerOptions、错误与告警处理程序等。

export interface AppConfig {
  // @private
  readonly isNativeTag?: (tag: string) => boolean
​
  performance: boolean
  optionMergeStrategies: Record<string, OptionMergeFunction>
  globalProperties: Record<string, any>
  errorHandler?: (
    err: unknown,
    instance: ComponentPublicInstance | null,
    info: string
  ) => void
  warnHandler?: (
    msg: string,
    instance: ComponentPublicInstance | null,
    trace: string
  ) => void
​
  /**
   * Options to pass to `@vue/compiler-dom`.
   * Only supported in runtime compiler build.
   */
  compilerOptions: RuntimeCompilerOptions
​
  /**
   * @deprecated use config.compilerOptions.isCustomElement
   */
  isCustomElement?: (tag: string) => boolean
​
  /**
   * Temporary config for opt-in to unwrap injected refs.
   * TODO deprecate in 3.3
   */
  unwrapInjectedRef?: boolean
}
​

3. AppContext

App的上下文,包含了对于componentsdirectivesmixinsprovidesconfig记录、对于propsemits的缓存、用于热更新的reload方法、兼容vue2filters记录等。

export interface AppContext {
  app: App // for devtools
  config: AppConfig
  mixins: ComponentOptions[]
  components: Record<string, Component>
  directives: Record<string, Directive>
  provides: Record<string | symbol, any>
​
  /**
   * Cache for merged/normalized component options
   * Each app instance has its own cache because app-level global mixins and
   * optionMergeStrategies can affect merge behavior.
   * @internal
   */
  optionsCache: WeakMap<ComponentOptions, MergedComponentOptions>
  /**
   * Cache for normalized props options
   * @internal
   */
  propsCache: WeakMap<ConcreteComponent, NormalizedPropsOptions>
  /**
   * Cache for normalized emits options
   * @internal
   */
  emitsCache: WeakMap<ConcreteComponent, ObjectEmitsOptions | null>
  /**
   * HMR only
   * @internal
   */
  reload?: () => void
  /**
   * v2 compat only
   * @internal
   */
  filters?: Record<string, Function>
}

4. Plugin

PluginPlugin中的install方法,基本和vue2一致。清晰可见,Plugin可以本身就是一个PluginInstallFunction类型函数,也可以是一个包含该类型函数的对象。

type PluginInstallFunction = (app: App, ...options: any[]) => anyexport type Plugin =
  | (PluginInstallFunction & { install?: PluginInstallFunction })
  | {
      install: PluginInstallFunction
    }

5. CreateAppFunction

CreateAppFunction就是我们的createApp函数的类型,接收一个根组件,以及一个可选参数rootProps对根组件进行传参。

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

二、createApp

从某种程度上可以说,Vue3的一切都是从createApp开始的。createApp这个API定义在packages/runtime-dom/src/index.ts文件里,接下来简要看一看它大致走了哪些流程。

1. createApp

  • 首先在ensureRenderer中调用createRenderer得到rendererrenderer上有createApp的方法,从而得到app
  • 重写app.mount方法,对app._componentcontainer的内容作处理;并且在其中调用原本的mount之前,先对container的内容进行清空。
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}
​
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) {
      // __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>

那么这里就会产生疑问,毕竟真正的createApp是在renderer上的。而renderer来自createRenderer,那么这个createRenderer又是如何创建renderer的呢?

createApp() -> ensureRenderer() -> createRenderer() => renderer -> renderer.createApp()

2. createRenderer

我们可以在packages/runtime-core/src/renderer.ts里找到createRenderer的定义。发现是调用了baseCreateRenderer。这个方法就比较长了,加上重载的话有2000+行,其中包含了patchmoveunmount等许多diff相关的方法,目前就不在这里展开了,只看一下它的返回值,追踪一下我们说的createApp的来源。可以看到,返回的renderer对象上有renderhydrate方法和createApphydrate是用于baseCreateRenderer的另一种重载,render方法就非常重要了,而createApp的来源是createAppAPI,这个API定义在packages/runtime-core/src/createAppAPI.ts里。

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}
​
// baseCreateRendererfunction baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    
  // 此处省略上万字
    
  // render方法,虽重要,但不是本文的主角,先露个脸吧
  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
  }
  
  // ...
    
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

3. createAppAPI

好家伙,走了这么长个流程,终于轮到主角登场了。在baseCreateRenderer的返回值中,我们可以看到,createApp方法就是以renderhydrate作为入参,提供给createAppAPI,从而诞生的。而这个近200行的函数,直接返回了我们用的createApp这个函数,这下子终于得到了真正的createApp。而逻辑也非常简单清晰:

  • 创建上下文context
  • 声明一个不可重复的插件容器;
  • 初始化isMounted状态为false
  • 创建app并挂到context上,最后返回app
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
      rootComponent = { ...rootComponent }
    }
​
    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()
​
    // 还没进行 mount() 呢,isMounted 自然是 false
    let isMounted = false
    
    // 终于创建了app了
    const app:App = (context.app={
      // ...
    })
    
    // 考虑兼容的属性
    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }
​
    return app
  }
}

那么重点就是创建的app了,让我们掀开它神秘的面纱。

  • 配置了一些内部属性;
  • 利用存取器配置了只读的config属性;
  • 定义了一些方法,如usemountcomponentdirectivemixinunmountprovide等,这时候回顾一下最开始我们说到的相关ts类型中的App类型,就对应上了。
  • 其中,componentdirectivemixinprovide都是用于定义一些全局可用的东西。这几个方法的逻辑也都一致,把定义的全局的内容添加到上下文context对象的相应字段中。
  • use:这个应该家喻户晓了,就是使用插件,调用其中的install方法或者插件本身(当插件本身就是一个函数且没有install方法时) 来安装插件,并且用installedPlugins来判断是否已安装;
  • mount:根据闭包的变量isMounted来判断app是否已经挂载;用根组件rootComponent作为参数,调用createVnode来生成根节点,并将上下文context也保存在vnode.appContext中;执行render函数将vnode渲染到rootContainer中,这一步我们应该很熟,就是替换innerHTML;之后变更isMounted状态为true等;
  • unmount:同样是执行render函数,只是这次是把null空值渲染到rootContainer中,用空的内容替换之前mount时渲染的内容,从而达到卸载应用的效果。
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) {
    if (__DEV__) {
      warn(
        `app.config cannot be replaced. Modify individual options instead.`
      )
    }
  },
​
  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)
    } else if (__DEV__) {
      warn(
        `A plugin must either be a function or an object with an "install" ` +
          `function.`
      )
    }
    return app
  },
​
  mixin(mixin: ComponentOptions) {
    if (__FEATURE_OPTIONS_API__) {
      if (!context.mixins.includes(mixin)) {
        context.mixins.push(mixin)
      } else if (__DEV__) {
        warn(
          'Mixin has already been applied to target app' +
            (mixin.name ? `: ${mixin.name}` : '')
        )
      }
    } else if (__DEV__) {
      warn('Mixins are only available in builds supporting Options API')
    }
    return app
  },
​
  component(name: string, component?: Component): any {
    if (__DEV__) {
      validateComponentName(name, context.config)
    }
    if (!component) {
      return context.components[name]
    }
    if (__DEV__ && context.components[name]) {
      warn(`Component "${name}" has already been registered in target app.`)
    }
    context.components[name] = component
    return app
  },
​
  directive(name: string, directive?: Directive) {
    if (__DEV__) {
      validateDirectiveName(name)
    }
​
    if (!directive) {
      return context.directives[name] as any
    }
    if (__DEV__ && context.directives[name]) {
      warn(`Directive "${name}" has already been registered in target app.`)
    }
    context.directives[name] = directive
    return app
  },
​
  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.`
        )
      }
      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 = () => {
          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
      // 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)``
      )
    }
  },
​
  unmount() {
    if (isMounted) {
      render(null, app._container)
      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        app._instance = null
        devtoolsUnmountApp(app)
      }
      delete app._container.__vue_app__
    } else if (__DEV__) {
      warn(`Cannot unmount an app that is not mounted.`)
    }
  },
​
  provide(key, value) {
    if (__DEV__ && (key as string | symbol) in context.provides) {
      warn(
        `App already provides property with key "${String(key)}". ` +
          `It will be overwritten with the new value.`
      )
    }
​
    context.provides[key as string | symbol] = value
​
    return app
  }
})

createApp流程基本都弄明白了,但是我们并不清楚render的过程是如何进行的。后续会抽时间解读render函数的故事。