theme: fancy


一. 前言

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

二. createApp 的创建

1. creatAppAPI

该方法接收两个参数,并且返回一个createApp,也就是我们在启动时使用的接口,我们先不关心该参数从哪来,我们只要知道它的作用即可,接着往下看

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

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

2. 参数 render

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

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

export type RootRenderFunction<HostElement = RendererElement> = (
  vnode: VNode | null,
  container: HostElement,
  isSVG?: boolean
) => void

从上述源码中我们可以了解到,render参数是一个Function类型,接收两个参数,一个是VNode虚拟dom,一个是container真实的dom元素,由此我们可以预想,render的作用是:将虚拟dom渲染到真实的dom中

3. 参数 hydrate

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

三. createApp 内部执行

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

//源码路径 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
  • _comonent:存放当前组件通过编译后的数据
  • _props:当前组件接受的参数
  • _container:当前组件对应的要渲染的真实dom位置
  • _context:当前组件上下文对象,其中包含config,app等
  • _instance:当前组件实例对象
  • config: 配置信息,也就是 context中的config信息,存放一些全局信息
  • installedPlugins:用来保存安装过的插件,使用Set代替了之前的数组,有效避免了重复安装
  • use:用来注册插件
  • mixin:用来混入全局数据
  • component:用于注册组件
  • mount:执行挂载操作
  • provide:为子组件提供可用参数
    接下来让我们一个个分析其详细实现。
  1. config
    config参数此处用了数据劫持的方式来设置获取和设置值,类似与Object.defineProperty,在 getter 中取到的是 context.config 的值,在 setter 中我们可以知道,app.config是不可以直接更改的,只可以改变其中的参数,让我们来看下其内部定义,接着往下看
//源码路径 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.`
          )
        }
      },
  1. _context
    该参数是由createAppContext方法返回,让我们看下其内部实现
    该方法很简单,直接返回了一个对象,其中包括 app,config等。
    通过对比vue2,在vue2中我们将组件的信息保存在vm.$options中,而vue3将所有数据存放在app._context中。
// 源码路径 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()
  }
}
  1. use
    该方法用来注册插件,将插件保存到installedPlugins中,该变量是一个Set类型,首先通过 installedPlugins.has来判断插件是否已经安装过,避免重复安装浪费性能,之后使用installedPlugins.add将插件存放起来,同时调用Plugin.install方法,将app作为参数导入,之后返回app,因此实现了链式调用。
// 源码路径 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
      },
  1. mixin
    该方法是向该组件的所有子组件中混入相同的 options,其实现也比较简单,将传入的options存入context中的mixins数组中,传入前检测数组中是否已经存在。该方法不同于vue2mixin的实现,vue2是将options混入到vue或者vueComponent的静态options当中。感兴趣的可以看下我的另一篇文章Vue.mixin 源码深入理解 - 掘金 (juejin.cn)
// 源码路径 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
      },
  1. component
    该方法用来注册组件,接收两个参数,第一个为组件名字,第二个参数是对应的组件,我们将其组件保存到context.components中,context.components是一个对象,键和值对应着我们的组件名和组件。
// 源码路径 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
      },
  1. provide
    该方法接收两个参数,要提供给子组件的数据对应的键值,保存到context.provides对象中。
// 源码路径 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
      }
  1. unmount
    该方法用来卸载组件,通过isMounted来过滤,只有已经过载的组件,才可以继续执行,在此处调用了render函数,我们在上文中提过,忘记的可以返回去看一下。此处传入一个空字符串,来替换该组件的内容,实现页面卸载。
// 源码路径 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.`)
        }
      },
  1. mount
    该API是最重要的,所以我们也放在压轴位置来讲。
    该方法传入三个参数,但是我们只讲第一个参数rootContainer,该参数值是一个真实dom元素,但是我们在使用时只是传入一个idapp.mount('#app'),而获取dom的过程则是在编译器compiler中完成。因此我们打印参数rootContainer的值并不是#app,而是一个真实的dom元素
    该方法内部,主要执行了两个API,先通过createVNode将我们的根组件App转换成VNode,然后执行render将虚拟dom渲染为真实dom。
    createVNodeVNode的具体实现将在后续文章中专门讲解,在此不做详细解释。
// 源码路径 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从构建到执行,到组件挂载全部执行完毕。大致过程如下:

生 成 r e n d e r − > c r e a t e A p p A P I − > c r e a t e A p p − > 初 始 化 并 构 建 a p p − > 执 行 a p p . m o u n t − > c r e a t e V N o d e − > r e n d e r 生成render -> createAppAPI -> createApp ->初始化并构建app -> 执行app.mount -> createVNode -> render render>createAppAPI>createApp>app>app.mount>createVNode>render
由此,我们的页面就会展现到浏览器中。\

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐