Vue3进阶之详解应用创建的过程

1,394 阅读4分钟

本文的目的就是从createApp 开始详细解读一下Vue3创建应用的过程。

创建应用

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

  <div id="root">
    <h1>{{text}}</h1>
  </div>

  <script>
    const app = Vue.createApp({
      data() {
        return {
          text: 'Hello Vue'
        }
      }
    })

    app.mount('#root')
  </script>
</body>

我们通过以上代码就能创建一个最简单的vue 应用(它的功能是在浏览器窗口中展示Hello Vueh1标签)。 上述过程一共做了2件事情:

  1. 通过 createApp 函数创建app对象
  2. 通过 app.mount 方法将内容挂载到 idroot 的标签上

app对象的产生

app 是Vue应用的本体,它由Vue的全局api createApp 生成。接下来让我们一起来看看 createApp 的内部实现,直到 app 的创建。 ​

createApp 函数位于vue-next/packages/runtime-dom/src/index.ts,它的类型定义为:

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

createApp 函数接收 rootComponent 根组件作为参数, rootProps 根组件的props作为可选参数。 具体实现为:

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
	
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // ... 此处逻辑省略
  }

  return app
}) as CreateAppFunction<Element>

createApp 内部流程如下:

  1. 利用 ensureRendered 创建 rendered 渲染器,并利用 rendered 上的 createApp 方法创建 app 对象。
  2. 重写 app 对象上的 mount 方法
  3. 返回 app 对象(即应用实例)

创建renderer

从以上流程我们能够知道 createApp 函数的内部是通过 rendered 渲染器来创建应用实例的,所以我们需要通过 createRenderer 函数来创建渲染器。即:

// vue-next/packages/runtime-dom/src/index.ts
let renderer: Renderer<Element> | HydrationRenderer

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

看到这里,也许有读者可能会和我对于 ensureRenderer 的实现有同样的疑惑,为什么 ensureRenderer 函数不直接返回 createRenderer 函数的调用而是像return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)) 来实现延迟创建渲染器呢? 这是因为Vue3中实现了全局api的 tree-shaking ,为了使得核心渲染逻辑能够 tree-shakable 而采用了延迟创建 rendered 。(我们是从以下注释知道的)

// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.

createRendered 内部调用了 createBaseRendered 来创建 rendered :

// vue-next/packages/runtime-core/src/renderer.ts
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

createBaseRendered 是创建 rendered 的底层函数,该函数的内部逻辑比较复杂,后面我有时间会单开一篇来具体分析一下内部流程。此时我们只要关注该函数的返回值:

// vue-next/packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // ... 此处逻辑省略
    return {
      render,
      hydrate,
      createApp: createAppAPI(render, hydrate)
    }
}

ok,我们终于看到了 ensureRenderer().createApp(...args)createApp 的来源。该属性的值是调用 createAppAPI 函数后的返回结果。

app应用实例来自于 createAppAPI

createAppAPI 的内部实现如下:

// vue-next/packages/runtime-core/src/apiCreateApp.ts
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) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },

      use(plugin: Plugin, ...options: any[]) {
        // ... 插件化
        return app
      },

      mixin(mixin: ComponentOptions) {
        // ... 全局mixin
        return app
      },

      component(name: string, component?: Component): any {
        // ... 全局组件注册
        return app
      },

      directive(name: string, directive?: Directive) {
       // ... 全局指令注册
        return app
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
       	// ... 应用挂载
      },

      unmount() {
        // ... 应用卸载
      },

      provide(key, value) {
        // ... 依赖注入
        return app
      }
    })

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

    return app
  }
}

从上述代码我们可以看到 createAppAPI 函数 返回 createApp 函数,而返回的 createApp 函数执行后就可以得到 app 应用实例,其中 app 对象进行了一系列初始化操作,并通过 createAppContext() 可以得到应用全局上下文 context,后面应用中所有的组件都可以进行访问。 ​

重写 app 对象上的 mount 方法

分析完渲染器的创建过程后,我们继续移步到 全局API createApp 内部流程的下一步:重写 app 对象上的 mount 方法。

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector) // 同时支持字符串和DOM对象
    if (!container) return
    const component = app._component
    // 若根组件非函数对象且未设置render和template属性,则使用容器的innerHTML作为模板的内容
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    container.innerHTML = '' // 在挂载前清空容器内容
    const proxy = mount(container) // 执行挂载操作
    if (container instanceof Element) {
      container.removeAttribute('v-cloak') // 避免在网络不好或加载数据过大的情况下,页面渲染的过程中会出现Mustache标签
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

app.mount 方法内部,当设置好根组件的相关信息之后,就会调用 app 对象原始的 mount 方法执行挂载操作。 那么为什么要重写 app.mount 方法呢?原因是为了支持跨平台,在 runtime-dom 包中定义的 app.mount 方法,都是与 Web 平台有关的方法。 ​

总结

ok,以上就是 createApp 内部的具体流程,我们会发现 Vue3 的内部实现虽然十分复杂,但是功能模块的分离非常清晰,且给开发者暴露的api 是如此简洁优雅,以致我们用 const app = createApp({}) 就可以创建一个应用实例。