Vue3源码解读之createApp

vue3 中,创建一个Vue应用实例是使用 createApp 方法:

html 复制代码
<script>
  Vue.createApp({
     setup () {
    const state = reactive({})

    return {
      state
    }
  },

  directives: {
    'todo-focus': (el, { value }) => {}
  }
  }).mount('#app')
</script>

那么,createApp 方法做了什么事情呢?它是如何把VNode转换成真实的DOM节点的呢?带着这些疑问,从文本开始,我们深入探究。

createApp

createApp 函数的定义在 packages/runtime-dom/src/index.ts 文件中,下面仅贴出关键代码:

createApp源码

js 复制代码
// packages/runtime-dom/src/index.ts 

// 这里的 createApp 方法是在写页面时实际调用的方法
export const createApp = ((...args) => {
  // 获取渲染器,并执行渲染器的 createApp 方法,创建 app 应用实例
  const app = ensureRenderer().createApp(...args)
  
  // ...
  // 扩展 mount 方法
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 获取根节点,即页面上 id 为 app 的div标签
    const container = normalizeContainer(containerOrSelector)
    
    //...
    
    const component = app._component
    // 获取模板
    if (!isFunction(component) && !component.render && !component.template) {
      // 将根节点下的HTML内容添加到组件的 template 上
      component.template = container.innerHTML
      // ...
    }
    
    const proxy = mount(container, false, container instanceof SVGElement)
    
    // ...
    
    return proxy
  }
  
  return app
}) as CreateAppFunction<Element>

我们首先来看看通过 createApp 创建的应用实例是怎样的:

可以看到,app 是一个包含component、config、directive、mixin、mount、provide等属性的实例对象,该实例对象提供了一个应用上下文,实例对象上的方法可以链式调用。

我们再来看看 createApp 的入参 App 组件实例的数据是怎样的:

可以看到,参数 args 就是我们在调用 createApp 传入的参数,并在此基础上添加了 render 函数和 setup 函数

下面,我们正式开始进入 createApp 的源码解读。

我们往上看 createApp 的核心代码,发现 createApp 只做了两件事情:

  1. 调用 ensureRenderer 函数获取渲染器,然后执行渲染器的 createApp 方法创建 app 应用实例。
js 复制代码
// 获取渲染器,并执行渲染器的 createApp 方法,创建 app 应用实例
const app = ensureRenderer().createApp(...args)
  1. 获取 app 应用实例的 mount 方法,对mount方法进行扩展。
js 复制代码
// 扩展mount方法
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
  // ...
}

创建应用实例

我们从 ensureRenderer 函数开始,看看一个Vue应用实例是如何创建出来的。

ensureRenderer

js 复制代码
// packages/runtime-dom/src/index.ts

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

ensureRenderer 函中,返回一个 renderer 渲染器,如果渲染器不存在,则调用 createRenderer 函数创建一个渲染器,并赋值给 renderer

createRenderer

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

在 createRenderer 函数中,调用 baseCreateRenderer 函数,传入 options,创建一个渲染器。

baseCreateRenderer

js 复制代码
// packages/runtime-core/src/renderer.ts

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {


  // ...

  // 返回的对象就是渲染器
  return {
    render, // 将传入vnode转换为dom并追加
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }

}

baseCreateRenderer 函中,返回了一个对象,这个对象就是渲染器,渲染器上有一个 render 方法,hydrate 属性和 createApp 方法。我们重点关注这个 createApp 方法,它被赋值了 createAppAPI 方法。

我们继续往下看 createAppAPI

createAppAPI

createAppAPI源码

js 复制代码
// packages/runtime-core/src/apiCreateApp.ts

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {

  // 返回开发者使用的app工厂函数
  return function createApp(rootComponent, rootProps = null) {
    // rootComponent 就是 createApp函数的 args,也就是options

    // ...

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

      // 加载插件,和vue2不同的是,vue2的插件是全局的,这里只针对一个vue实例
      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) {
        // ...
      }
    })

    return app
  }
}

createAppAPI 函数中,返回的工厂函数 createApp 就是开发者在页面中调用的createApp方法,在工厂函数中创建了一个app应用程序实例,并将其添加到context对象的app属性上,最后将其返回出去。

创建流程

创建app应用实例的流程如下图:

扩展 mount 方法

在创建完app应用程序实例后,会取出app上的mount方法,对其进行扩展。

扩展mount源码

js 复制代码
// packages/runtime-dom/src/index.ts 

// 扩展 mount 方法
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
  // containerOrSelector: #app 
  // 获取根节点,即页面上 id 为 app 的div标签
  const container = normalizeContainer(containerOrSelector)

  //...

  // _component 上存储的是 createApp 这个 API 的参数 args
  const component = app._component
  // 获取模板
  // component 是createApp 这个 API 的参数 args,传入的是一个对象
  // 首次初始化时 component 上没有 render 属性 和 template 属性
  if (!isFunction(component) && !component.render && !component.template) {
    // 将根节点下的HTML内容添加到组件的 template 上
    component.template = container.innerHTML
    // ...
  }

  const proxy = mount(container, false, container instanceof SVGElement)

  // ...

  return proxy
}

我们在上文中说到的createApp 的入参 args 中的 template 就是在 mount 方法的扩展中添加上去的。

下面我们继续来看看 mount 方法。

mount

mount源码

mount 方法的定义在 createAppAPI 函数返回的开发者使用的app工厂函数createApp中,mount用来挂载节点,将根组件下的HTML内容渲染出来,并建立更新机制。

js 复制代码
// packages/runtime-core/src/apiCreateApp.ts

// 挂载,核心渲染逻辑,将 vnode 转换成真实DOM
// 注意:mount方法只会执行一次
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  // 判断是否已挂载
  if (!isMounted) {
    // 首次挂载,创建根组件对应的vnode,即虚拟DOM
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    // appContext应用程序上下文是一个全局的对象,用于存储应用程序级别的配置和实例。在使用 createApp 函数创建应用程序时,会创建一个应用程序上下文对象,并将其传递给根组件实例。
    
    // 将上下文信息挂载到根组件节点的 appContext 属性上,
    vnode.appContext = context

    // 开发环境下 热更新
    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer, isSVG)
      }
    }

    // 转换 VNode 的处理逻辑
    if (isHydrate && hydrate) {
      // ssr 服务端渲染 的 VNode 转换逻辑
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      // spa 前端渲染 的 VNode 转换逻辑
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    // for devtools and telemetry
    ;(rootContainer as any).__vue_app__ = app

    // 删除了 __DEV__ 部分的代码

    return getExposeProxy(vnode.component!) || vnode.component!.proxy
  } else if (__DEV__) {
   // 删除了 __DEV__ 部分的代码
  }
},

mount 方法只会执行一次,即在app应用实例挂载时执行。它调用createVNode函数创建根组件对应的VNode,然后判断是否是服务端渲染,如果是,则执行外部传入的hydrate方法,将VNode转换成真实DOM;如果不是,则调用外部传入的render方法,将将VNode转换成真实DOM,将HTML内容渲染出来。

下面我们来看看 render 方法。

render

js 复制代码
// packages/runtime-core/src/renderer.ts

const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      // 卸载组件
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 执行 patch过程,即Diff过程
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}

可以看到,如果vnode不为null,即根组件对应的vnode存在,则执行 patch 过程,即 Diff 过程。patch 过程将在下篇文章讲解。

createApp 的调用流程

在页面中调用 createApp 时发生的函数调用流程如下:

总结

开发者在调用 createApp 时,createApp做了两件事情:

  1. 调用 ensureRenderer 函数获取渲染器,然后执行渲染器的 createApp 方法创建 app 应用实例。
  2. 在创建完app应用程序实例后,会取出app上的mount方法,对其进行扩展。在mount方法中,创建根节点的VNode,进入patch过程 (即Diff过程),并建立更新机制。
相关推荐
田本初5 分钟前
如何修改npm包
前端·npm·node.js
明辉光焱26 分钟前
[Electron]总结:如何创建Electron+Element Plus的项目
前端·javascript·electron
牧码岛1 小时前
Web前端之汉字排序、sort与localeCompare的介绍、编码顺序与字典顺序的区别
前端·javascript·web·web前端
开心工作室_kaic1 小时前
ssm111基于MVC的舞蹈网站的设计与实现+vue(论文+源码)_kaic
前端·vue.js·mvc
晨曦_子画1 小时前
用于在 .NET 中构建 Web API 的 FastEndpoints 入门
前端·.net
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:在 PDF 文件中添加图像作为页面背景
前端·pdf·.net·spire.pdf
咔咔库奇1 小时前
ES6基础
前端·javascript·es6
Jiaberrr2 小时前
开启鸿蒙开发之旅:交互——点击事件
前端·华为·交互·harmonyos·鸿蒙
bug爱好者2 小时前
如何解决sourcetree 一打开就闪退问题
前端·javascript·vue.js