2026.01.18

createApp -> Vue 应用的初始化与挂载机制

在前面的章节中,我们实现了 render 函数来渲染虚拟节点。然而,Vue 的实际使用中更常见的是 createApp API。本章将深入 createApp 的实现原理,理解它如何封装渲染逻辑,并提供更友好的应用级 API。

在前面的章节中,我们实现了 render 函数来渲染虚拟节点。然而,Vue 的实际使用中更常见的是 createApp API。本章将深入 createApp 的实现原理,理解它如何封装渲染逻辑,并提供更友好的应用级 API。

从 createRenderer 说起

createRenderer 的完整返回值

回顾我们之前实现的 createRenderer,它的返回值不仅仅是 render 函数,还包括 createApp

createApp 的使用方式

让我们先回顾一下 createApp 在实际开发中的典型用法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createApp</title>
</head>

<body>
<div id="app"></div>
<script type="module">
    import {h, createApp} from '../dist/vue.esm.js'

    const Comp = {
        render() {
            return h('div', 'hello')
        }
    }

    createApp(Comp, {message: 'msg'}).mount('#app')
</script>
</body>
</html>

设计思路分析

createApp 与 render 的关系

思考一下:createAppmount 方法本质上做了什么?

它将组件渲染到容器中,这不正是 render 函数的职责吗?

// render 函数的作用
render(vnode, container) // 将虚拟节点渲染到容器中

// createApp 的作用
createApp(component, props).mount(container) // 将组件挂载到容器中

因此,createApp 是对 render 的高层封装,提供更完善的应用管理能力。

架构设计

createRenderer 中,我们这样组织代码:

export function createRenderer(options) {
  const render = (vnode, container) => {
    // 渲染逻辑
  }

  return {
    render,
    createApp: createAppApi(render)
  }
}

设计要点:

  1. 依赖注入createAppApi 接收 render 函数作为依赖
  2. 关注点分离createAppApi 专注于应用级 API,不关心渲染细节
  3. 函数工厂createAppApi 返回 createApp 函数

实现 createAppApi

核心实现

packages/runtime-core/src/apiCreateApp.ts 中创建 createAppApi 函数:

/**
 * 创建 createApp 函数的工厂
 * @param render - 渲染器的 render 函数
 * @returns createApp 函数
 */
export function createAppApi(render) {
  /**
   * 创建应用实例
   * @param rootComponent - 根组件对象
   * @param rootProps - 根组件的 props
   * @returns 应用实例对象
   */
  return function createApp(rootComponent, rootProps) {
    const app = {
      _container: null, // 保存挂载的容器引用

      /**
       * 挂载应用到指定容器
       * @param container - DOM 容器元素
       */
      mount(container) {
        // 1. 将组件转换为虚拟节点
        const vnode = createVNode(rootComponent, rootProps)

        // 2. 调用 render 函数渲染虚拟节点
        render(vnode, container)

        // 3. 保存容器引用,用于 unmount
        app._container = container
      },

      /**
       * 卸载应用
       */
      unmount() {
        // 渲染 null 来卸载所有内容
        render(null, app._container)
      }
    }

    return app
  }
}

unmount 方法的实现原理

为什么 render(null, container) 可以卸载?

在我们的渲染器实现中,当新节点为 null 时会触发卸载逻辑:

// 在 patch 函数中
if (vnode == null) {
  if (container._vnode) {
    unmount(container._vnode) // 卸载旧节点
  }
}

增强 mount 方法:支持 CSS 选择器

问题场景

当前的实现有一个限制:

// ✅ 可以工作
const el = document.querySelector('#app')
app.mount(el)

// ❌ 会报错
app.mount('#app')

但在实际使用中,我们希望像 Vue 官方那样直接传入选择器字符串。

解决方案:平台特定的封装

架构约束:

  • runtime-core 是平台无关的,不能直接操作 DOM
  • runtime-dom 是浏览器平台特定的实现

因此,我们需要在 runtime-dom 中对 createApp 进行增强。

在 runtime-dom 中增强

packages/runtime-dom/src/index.ts 中:

import { createRenderer } from '@vue/runtime-core'
import { isString } from '@vue/shared'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'

export * from '@vue/runtime-core'

const renderOptions = { patchProp, ...nodeOps }
const renderer = createRenderer(renderOptions)

/**
 * 浏览器平台的 render 函数
 */
export function render(vnode, container) {
  renderer.render(vnode, container)
}

/**
 * 浏览器平台增强版的 createApp
 * 支持 CSS 选择器作为挂载目标
 */
export function createApp(rootComponent, rootProps) {
  // 1. 调用 runtime-core 的 createApp
  const app = renderer.createApp(rootComponent, rootProps)

  // 2. 保存原始的 mount 方法
  const _mount = app.mount.bind(app)

  // 3. 覆盖 mount 方法,增加选择器支持
  function mount(selector) {
    let container = selector

    // 如果是字符串,当作 CSS 选择器处理
    if (isString(selector)) {
      container = document.querySelector(selector)

      if (!container) {
        console.warn(`Failed to mount app: mount target selector "${selector}" returned null.`)
        return
      }
    }

    // 调用原始 mount 方法
    _mount(container)
  }

  // 4. 替换 mount 方法
  app.mount = mount

  return app
}

export { renderOptions }

分层设计的优势

通过这种分层设计,我们获得了:

1. 平台无关性

// runtime-core: 纯逻辑,不依赖浏览器 API
export function createAppApi(render) {
  return function createApp(rootComponent, rootProps) {
    // ...
  }
}

2. 平台特定增强

// runtime-dom: 浏览器特定功能
export function createApp(rootComponent, rootProps) {
  const app = renderer.createApp(rootComponent, rootProps)
  // 增强 mount 方法
}

3. 可扩展性

如果要支持其他平台(如小程序、Native),只需:

  • 复用 runtime-core
  • 创建新的 runtime-xxx
  • 实现平台特定的 nodeOpscreateApp 增强

完整代码汇总

apiCreateApp.ts (runtime-core)

/**
 * 创建 createApp API 的工厂函数
 * 平台无关的应用实例管理
 */
export function createAppApi(render) {
  return function createApp(rootComponent, rootProps) {
    const app = {
      _container: null,

      mount(container) {
        // 组件 -> 虚拟节点 -> 渲染
        const vnode = createVNode(rootComponent, rootProps)
        render(vnode, container)
        app._container = container
      },

      unmount() {
        // 渲染 null 触发卸载
        render(null, app._container)
      }
    }

    return app
  }
}

index.ts (runtime-dom)

/**
 * 浏览器平台的 createApp 增强
 * 支持 CSS 选择器作为挂载目标
 */
export function createApp(rootComponent, rootProps) {
  const app = renderer.createApp(rootComponent, rootProps)
  const _mount = app.mount.bind(app)

  function mount(selector) {
    let container = selector
    if (isString(selector)) {
      container = document.querySelector(selector)
      if (!container) {
        console.warn(`Failed to mount app: mount target selector "${selector}" returned null.`)
        return
      }
    }
    _mount(container)
  }

  app.mount = mount
  return app
}

总结

至此,我们完成了 createApp 的核心实现,为后续的组件等奠定了基础。这个应用级 API 将成为整个 Vue 应用的入口点。