Vue 源码解析(五):createApp

710 阅读6分钟

Vue createApp 源码解析

概念介绍

先对本文提到的一些概念做一些介绍:

  • 应用实例:和下面的组件实例一样,本质都是js对象。上面记录着app运行过程中所需的属性和方法,例如mountunmountcomponent
  • 组件实例:本质是一个js对象,在组件挂载时会被创建,上面存在着datapropsemit等属性(详见runtime-core/component.tscreateComponentInstance函数)。内部的具体值由组件对应的vnode(即用户为组件传入了哪些值)和组件的定义(例如组件定义了class这个prop,则用户传入的:class会被认为是props,否则会被认为是attrs)决定。

引入

Vue通过createApp来创建应用实例app,并通过app.component来注册组件,app.mount来挂载组件,app.unmount来卸载组件。那么,这些API的原理是什么呢?本文将通过以下例子对其原理进行讲解:

<script src="../../dist/vue.global.js"></script>

<div id="demo">
  <div>{{ value }}</div>
  <Children></Children>
</div>

<script>
  const { createApp, ref } = Vue

  const app = createApp({ // 创建应用实例
    setup() {
      const value = ref(0)
      return { value }
    }
  })
  app.component( // 注册一个组件
    'Children',
    // 组件的定义是一个选项对象
    {
      template: `
      <div>Hello World.</div>
      `
    }
  )
  app.mount('#demo') // 挂载应用
</script>

createApp 源码解析

在上述代码中,首先会通过createApp API创建应用实例,该函数位于runtime-dom模块中:

export const createApp = ((...args) => {
  // 首先通过 ensureRenderer 获得全局对象 renderer,然后调用它的 createApp 方法来创建 app
  const app = ensureRenderer().createApp(...args)
	// 省略其他代码
  
  return app
}) as CreateAppFunction<Element>

该函数会通过exsureRenderer来创建renderer对象,由于renderer只负责渲染逻辑,因此这里使用了单例模式,让全局只存在一个renderer

let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

function ensureRenderer() {
  // 存在 renderer 则直接返回,否则创建后返回
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

renderer对象暴露出来的方法很少,但是其内部执行的逻辑却是Vue的核心(也是大部分面试题问的东西),其主要负责组件的渲染(包括组件挂载、更新、卸载......),关于它的具体内容,我会放在之后的renderer文章中,这里我们只看他所暴露出来的方法:

  • render:用来渲染组件,app.mount方法中使用到了它
  • hydrateSSR相关,这里不管它
  • createApp:调用runtime-core模块中的createAppAPI函数,并传入render函数,来创建应用实例
return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate)
}

接下来,就来到了本文的核心,也就是真正执行创建app逻辑的地方:createAppAPI函数

export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 省略一些逻辑
    const context = createAppContext()
    const app: App = (context.app = {
      // 为了方便展示,省略了对象属性以及方法的逻辑代码
   
      get 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) {},

      runWithContext(fn) {
      })
   
    return app
  }
}

这里的createApp接受的参数rootComponent(对应例子中的setup定义的根组件)会作为闭包变量被app的方法访问,而返回的app就是我们在例子中拿到的app对象(除了mount方法不一样),它上面定义了mountunmountcomponent等方法。

其中contextcreateApp函数中的闭包变量,它用来记录一些app对象方法执行中会用到的属性,例如组件、指令、mixins等等:

export function createAppContext(): AppContext {
  // context 的结构
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null),
    optionsCache: new WeakMap(),
    propsCache: new WeakMap(),
    emitsCache: new WeakMap()
  }
}

接下来我们着重看看其中几个方法的实现。

mount

mount方法的代码如下(省略了一些不太重要的逻辑,主要是关于devtools和开发环境下相关的代码):

mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  if (!isMounted) {
    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

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

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

首先根据闭包变量isMounted判断app是否已经挂载过了,因为每个应用实例仅能挂载一次,因此只有没有挂载过才可以执行逻辑。之所以把isMounted作为闭包变量而不是app的属性是因为不希望用户可以访问和更改该变量。

接着使用createVNode创建createApp参数对应的vnode。对应到例子中,我们使用setup选项定义了根组件,所以这里就是生成根组件对应的vnode

然后把context中的内容赋给vnode,因此vnode就拥有了根组件对应的组件、指令等信息。

接下来使用render方法,对虚拟节点进行渲染,并插入到rootContainer(对应例子中的#demo元素)中。

最后使用app._container记录下app挂载的节点,方便在unmount中进行卸载。

最后返回根组件的实例,但是这个返回值开发当中一般也用不到,所以不是很重要。

unmount

unmount的代码如下(依然省略一些不重要的逻辑):

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

核心逻辑就一行,也就是调用render函数。其中vnodenull,因此进而会调用renderer逻辑中的unmount进行卸载。

component

component的代码如下(依然省略一些不重要的逻辑):

component(name: string, component?: Component): any {
  if (!component) {
    return context.components[name]
  }
  context.components[name] = component
  return app
}

这个逻辑很好理解,如果app没有传入组件选项,说明是获得组件名对应的实例,则直接返回;否则就把组件的名称和组件实例记录到contextcomponents属性中。

顺便一提,directivemixinprovide方法的逻辑和component方法相同,都是使用context记录信息(指令、mixin或注入的依赖的值)。当执行具体的相关逻辑时,则会通过app._contextvnode.appContext来访问到context,获取到相关信息,然后执行逻辑。

use

app.useVue插件系统中的方法,用来安装插件(即拥有 install() 方法的对象,或是安装函数本身),使用方法如下:

// main.js
import { createApp } from 'vue'

const app = createApp({})

app.use(myPlugin, {
  /* 可选的选项 */
})

// myPlugin.js
const myPlugin = {
  install(app, options) {
    app.config.globalProperties.$route = {
      // 配置对象
    }
  }
}

use方法的源码如下:

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
}

其中installedPlugins是闭包集合对象,用来存储安装过的插件。如果一个插件没有被安装过,就调用它的install方法或者直接调用它,并传入app对象和可选参数。

provide

app.provideVue依赖注入的API,通过该方法可以实现应用级别的依赖注入,例如:

// main.js
app.provide('message', 'hello')

// App.vue
<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

这部分的源码如下所示。和component方法类似,provide方法只是将注入的依赖存储在context.provides中,而不执行具体逻辑。具体逻辑会在根组件创建时执行(位于component.ts/createComponentInstance中),可参见我的讲解provideinject的文章。

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会记录传入的根组件rootComponent,并返回app对象。该对象上的mount方法会调用createVNoderender等函数,利用context对象进行根组件的渲染;unmount方法同样是使用render函数进行卸载;component等方法会将信息记录到context对象中,而不执行具体的逻辑。
  • Vue的插件系统的思想:Vue的插件系统通过app.use接受带有install方法的对象或安装函数(即插件),并将app和额外参数交给插件。把app交给插件本质上是一种依赖注入,这体现了控制反转(IOC)的思想,即把应用实例的控制权交给插件,让插件可以调用app的方法或是增加全局属性。
  • 逻辑封装:
    • Vue把渲染相关的逻辑都封装在了renderer.ts中,并通过renderer对象暴露出运行所需的方法rendercreateApp,而createApp方法实际上又是通过调用apiCreateApp.ts中的createAppAPI函数来实现的。这样就把渲染的逻辑创建应用的逻辑封装到了不同的文件中。
    • app的方法调用过程(创建应用的逻辑)中会调用render方法(渲染的逻辑),但是并不关心render如何实现,而只需要知道它的功能和函数签名,具体的逻辑交给renderer即可。通过这种封装方式,降低了代码的侵入性。即便有一天渲染的逻辑发生了改变,也并不会影响到创建应用逻辑的代码。