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过程),并建立更新机制。
相关推荐
xiao-xiang6 分钟前
jenkins-通过api获取所有job及最新build信息
前端·servlet·jenkins
C语言魔术师23 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳2 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?2 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒9 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5