Vue3源码 | createApp都干了什么?

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

创建Vue应用实例,是一个项目的开始,这篇文章看看createApp内部都发生了什么。当然如果你对Vue3响应式系统感兴趣,也可以先看看这两篇文章:

Vue3源码 | 深入理解响应式系统上篇-reactive

Vue3源码 | 深入理解响应式系统下篇-effect

VUE3使用

这里看下Vue3是如何进行应用初始化。

// vue3 应用初始化
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')

// 组件渲染和未捕获错误配置的处理程序
app.config.errorHandler = (err, vm, info) => {}
// 添加全局属性
app.config.globalProperties.$http = () => {} // 这里相当于挂载到Vue2的 Vue.prototype
// 指定一种方法识别Vue之外定义的自定义元素
app.config.isCustomElement = tag => tag.startsWith('ion-')
// 注册组件
app.component('my-component', {})
// 检索组件
const MyComponent = app.component('my-component')
// 注册指令
app.directive('my-directive',{})
// 设置一个可以注入到应用程序内所有组件中的值。组件应使用inject来接收提供的值。
app.provide('user', 'administrator')
// 卸载应用程序
 app.unmount()
// 安装vue插件
import MyPlugin from './plugins/MyPlugin'
app.use(MyPlugin)
...
复制代码

Vue3跟Vue2初始化区别不大,都是创建应用实例,然后挂载到DOM节点上。这里也可以看出,Vue是通过JS渲染页面,跟传统页面DOM直出的方式是不一样。DOM直出,简单说是,请求后返回的HTML页面是最终的呈现效果。

createApp流程

createApp

createApp用于创建应用实例,下面看看内部代码实现:

export const createApp = ((...args) => {
  // 创建app实例
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  // 重写mount方法
  app.mount = (containerOrSelector: Element | string): any => {
    // 标准化容器
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    // 组件不存在render函数和模板template,则使用container的innerHTML做为组件模板
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // 挂载前,清空容器的内容
    container.innerHTML = ''
    // 挂载容器
    const proxy = mount(container)
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
    return proxy
  }

  return app
}) as CreateAppFunction<Element>
复制代码

通过上面源码解析,我们可以看出createApp主要是干了两件事:

  1. 创建app实例,并返回该实例
  2. 重写mount方法

看完会存在两个主要疑问,ensureRenderer 是干啥用的?为什么要重写 mount 方法,而不直接使用呢?

ensureRenderer

用于创建渲染器,渲染器核心代码处于 runtime-core/src/renderer.ts 文件。

baseCreateRenderer// 可以看出render存在2种类型的渲染器
let renderer: Renderer<Element> | HydrationRenderer

// 延迟创建渲染器,当用户只是使用reactive响应库,可以做tree-shaking优化
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

// HydrationRenderer渲染器,也是只在调用的时候创建,方便做tree-shaking优化
export function createHydrationRenderer(
  options: RendererOptions<Node, Element>
) {
  return baseCreateRenderer(options, createHydrationFunctions)
}
复制代码

从分析可以看出:

  1. 存在2种类型的渲染器,它们都是基于 baseCreateRenderer 函数创建,此函数存在重载。
  2. 渲染器都是通过延迟创建,方便不使用的时候做 tree-shaking
baseCreateRenderer

这个函数是个大家伙,1800行左右的代码,包含了组件渲染的核心逻辑。这里只抽离挂载过程相关逻辑,涉及组件更新等其他渲染逻辑,留到后面再深入研究。

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
   
   // 其他关于组件渲染逻辑以及hydrate相关这里省略
   
   // 渲染函数
   const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
      // 虚拟节点不存在,则销毁
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 虚拟节点存在,则更新或创建
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    // 缓存虚拟节点数据,作为已完成渲染的标识
    container._vnode = vnode
  }
  
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}
复制代码

可以看出 baseCreateRenderer 主要实现了:

  1. 实现了组件渲染的创建、更新、卸载等核心逻辑(后续解读)
  2. 返回渲染函数,以及创建应用实例方法,当然还有 hydrate
createAppAPI

组件渲染核心部分后面看,这里看看创建实例的API实现。

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  // createApp接收2个参数,根组件和根组件的属性
  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
		
    // 重写上下文对象的app属性
    const app: App = (context.app = {
      _component: rootComponent as Component,
      _props: rootProps,
      _container: null,
      _context: context,

      version,
			// 暴露的应用实例的配置
      get config() {
        return context.config
      },

      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },
			// 插件的安装,可以看出,插件如果是对象,必须有install方法;如果是函数,则默认是安装方法
      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)
          }
        }
        return app
      },

      component(name: string, component?: PublicAPIComponent): 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): any {
        if (!isMounted) {
          // 根据根组件创建虚拟节点
          const vnode = createVNode(rootComponent as Component, rootProps)
          vnode.appContext = context

          // HMR root reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer)
            }
          }
 
          render(vnode, rootContainer)
          
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          return vnode.component!.proxy
        }
      },

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

      provide(key, value) {
        context.provides[key as string] = value
        return app
      }
    })

    return app
  }
}
复制代码

从上面的代码,我们可以了解到createAppAPI主要实现了:

  1. 创建定义一个实例上下文context,包含属性和方法
  2. 重写扩展context.app方法,实现用户可以对上下文相关属性的自定义操作,也就是应用实例暴露的api实现,比如自定义指令、混入mixin、组件等提供用户自定义实现。
  3. 根据根组件和属性在 mount 方法中完成虚拟节点 vNode 的转换,并通过 render 喊完成渲染,关于渲染函数在 baseCreateRender 已经说过。

总结

至此分析了createApp大致的流程,内部更细致的实现,后续再根据内容深入分析,这里再总结下整个过程。

  1. 执行 createApp 首先会创建渲染器,这里要注意的是存在2种渲染器类型,并且它们都是通过延迟创建的,主要目的是当用户只引用reactive响应式框架的时候,方便进行tree-shaking优化。且两种渲染器都是基于 baseCreateRender 方法来实现。
  2. baseCreateRender 函数执行后会返回 render 渲染函数和 createApp 方法,其中 render 函数是组件创建、更新和卸载的主要核心逻辑实现。createApp则用于创建应用实例,进行应用实例的初始化。
  3. createAppAPI用于生成默认的应用上下文 context,这里定义了应用实例具备的属性和方法,并通过重写扩展 context.app 属性,让用户能够进行对上下文的自定义操作,比如自定义组件、指令、mixin、插件安装等一系列操作。并存在mount方法完成将根组件转为虚拟节点 vNode,并通过render 函数完成对 vNode 的渲染。

下一篇,我们将立足这篇文章,接着深入render函数内部,先看看vNode是什么,再瞧瞧DOM Diff过程。

猜你喜欢

转载自juejin.im/post/7032240868060823583