createApp - vue3源码解读

语言: CN / TW / HK

theme: fancy


一. 前言

vue3 已经成为主流,vue3 对 vue2 做了兼容的基础上,增加了大量响应式API(hook),更改了生命周期钩子,对响应式原理也做了优化,用 proxy 代替了之前的defineProperty,同时使用createApp的方式代替了之前使用new来启动的方式。好了,进入今天的主题,让我们看下createApp的由来以及它内部发生了什么?

二. createApp 的创建

1. creatAppAPI

该方法接收两个参数,并且返回一个createApp,也就是我们在启动时使用的接口,我们先不关心该参数从哪来,我们只要知道它的作用即可,接着往下看 ```js //源码路径 core/packages/runtime-core/src/apiCreateApp.ts

export function createAppAPI( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction { return function createApp (){ //... } } ```

2. 参数 render

我们想了解它的作用,首先看一下它的类型RootRenderFunction

```js //源码路径 core/packages/runtime-core/src/renderer.ts

export type RootRenderFunction = ( vnode: VNode | null, container: HostElement, isSVG?: boolean ) => void `` 从上述源码中我们可以了解到,render参数是一个Function类型,接收两个参数,一个是VNode虚拟dom,一个是container真实的dom元素,由此我们可以预想,render`的作用是:将虚拟dom渲染到真实的dom中

3. 参数 hydrate

该参数的作用是用于服务器渲染,此文先不做过多讲解。

三. createApp 内部执行

该方法最终返回一个app,类似于我们vue2中的vm实例,让我们先来总结下app内部有些什么参数吧(仅展示部分源码,详细实现往下看)

```js //源码路径 core/packages/runtime-core/src/apiCreateApp.ts

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
  },

  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

} ```

1. app

  • uid:用于标识组件唯一id
  • _component:存放当前组件通过编译后的数据
  • _props:当前组件接受的参数
  • _container:当前组件对应的要渲染的真实dom位置
  • _context:当前组件上下文对象,其中包含config,app等
  • _instance:当前组件实例对象
  • config: 配置信息,也就是 context中的config信息,存放一些全局信息
  • installedPlugins:用来保存安装过的插件,使用Set代替了之前的数组,有效避免了重复安装
  • use:用来注册插件
  • mixin:用来混入全局数据
  • component:用于注册组件
  • mount:执行挂载操作
  • provide:为子组件提供可用参数 接下来让我们一个个分析其详细实现。

  • config config参数此处用了数据劫持的方式来设置获取和设置值,类似与Object.defineProperty,在 getter 中取到的是 context.config 的值,在 setter 中我们可以知道,app.config是不可以直接更改的,只可以改变其中的参数,让我们来看下其内部定义,接着往下看 ```js //源码路径 core/packages/runtime-core/src/apiCreateApp.ts

    get config() { return context.config },

    set config(v) { if (DEV) { warn( app.config cannot be replaced. Modify individual options instead. ) } }, 3. `_context` 该参数是由`createAppContext`方法返回,让我们看下其内部实现\ 该方法很简单,直接返回了一个对象,其中包括 app,config等。 通过对比`vue2`,在`vue2`中我们将组件的信息保存在`vm.$options`中,而`vue3`将所有数据存放在`app._context`中。js // 源码路径 core/packages/runtime-core/src/apiCreateApp.ts

function createAppContext(): AppContext { 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() } } 4. `use` 该方法用来注册插件,将插件保存到`installedPlugins`中,该变量是一个`Set`类型,首先通过 `installedPlugins.has`来判断插件是否已经安装过,避免重复安装浪费性能,之后使用`installedPlugins.add`将插件存放起来,同时调用`Plugin.install`方法,将`app`作为参数导入,之后返回`app`,因此实现了链式调用。js // 源码路径 core/packages/runtime-core/src/apiCreateApp.ts

  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
  },

5. `mixin` 该方法是向该组件的所有子组件中混入相同的 `options`,其实现也比较简单,将传入的**options**存入`context`中的mixins数组中,传入前检测数组中是否已经存在。该方法不同于`vue2`中`mixin`的实现,`vue2`是将**options**混入到`vue`或者`vueComponent`的静态**options**当中。感兴趣的可以看下我的另一篇文章[Vue.mixin 源码深入理解 - 掘金 (juejin.cn)](https://juejin.cn/post/7092575288952881160)。js // 源码路径 core/packages/runtime-core/src/apiCreateApp.ts

  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
  },

6. `component` 该方法用来注册组件,接收两个参数,第一个为组件名字,第二个参数是对应的组件,我们将其组件保存到`context.components`中,`context.components`是一个对象,键和值对应着我们的组件名和组件。js // 源码路径 core/packages/runtime-core/src/apiCreateApp.ts

  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
  },

7. `provide` 该方法接收两个参数,要提供给子组件的数据对应的键值,保存到`context.provides`对象中。js // 源码路径 core/packages/runtime-core/src/apiCreateApp.ts

  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] = value

    return app
  }

8. `unmount` 该方法用来卸载组件,通过`isMounted`来过滤,只有已经过载的组件,才可以继续执行,在此处调用了`render`函数,我们在上文中提过,忘记的可以返回去看一下。此处传入一个空字符串,来替换该组件的内容,实现页面卸载。js // 源码路径 core/packages/runtime-core/src/apiCreateApp.ts

  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.`)
    }
  },

9. `mount` 该API是最重要的,所以我们也放在压轴位置来讲。\ 该方法传入三个参数,但是我们只讲第一个参数`rootContainer`,该参数值是一个真实**dom元素**,但是我们在使用时只是传入一个id`app.mount('#app')`,而获取**dom**的过程则是在编译器`compiler`中完成。因此我们打印参数**rootContainer**的值并不是`#app`,而是一个真实的**dom元素**。\ 该方法内部,主要执行了两个API,先通过`createVNode`将我们的根组件`App`转换成`VNode`,然后执行`render`将虚拟dom渲染为真实dom。\ `createVNode`和`VNode`的具体实现将在后续文章中专门讲解,在此不做详细解释。js // 源码路径 core/packages/runtime-core/src/apiCreateApp.ts

  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) // 其中执行了 setup
      }
      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)\``
      )
    }
  },

```

四.总结

到此,我们的createApp从构建到执行,到组件挂载全部执行完毕。大致过程如下:

$生成render -> createAppAPI -> createApp ->初始化并构建app -> 执行app.mount -> createVNode -> render$ 由此,我们的页面就会展现到浏览器中。\