Skip to content

Vue 3 createApp 到底干了啥?深度剖析架构设计与渲染初始化

如果你用过 Vue 2,一定对 new Vue() 这个经典的初始化方式印象深刻。但到了 Vue 3,一切都变了——我们现在用的是 createApp()。这不仅仅是 API 的改变,更是 Vue 架构层面的一次深刻重构。

很多人只知道怎么用 createApp,却不清楚它背后做了什么。为什么要从 Class 实例化改成工厂函数?为什么要分 runtime-domruntime-core 两层?app.mount() 在 DOM 层被重写,又有什么深意?

本文将带你深入 Vue 3 源码,从架构设计模式的角度,彻底搞清楚 createApp 到底干了啥

从 Vue 2 到 Vue 3:范式转移

Vue 2 的全局污染问题

在 Vue 2 中,所有的应用实例共享同一个 Vue 构造函数:

javascript
import Vue from 'vue'

Vue.use(VueRouter)  // 全局注册插件
Vue.mixin({ ... })  // 全局混入
Vue.component('MyComponent', { ... })  // 全局组件

const app1 = new Vue({ ... })
const app2 = new Vue({ ... })

这种设计存在严重的问题:

  1. 全局状态污染Vue.useVue.mixin 等操作会修改 Vue 构造函数的原型,导致所有实例共享这些全局配置
  2. 多应用隔离困难:在同一页面中创建多个独立的 Vue 应用时,它们之间会相互影响
  3. 测试不友好:单元测试时,全局状态会在测试用例之间泄漏,需要手动清理

举个实际例子,假设你在一个页面中需要同时渲染两个独立的应用:

javascript
// Vue 2 中无法做到真正隔离
Vue.use(PluginA)  // 影响所有应用
const app1 = new Vue({ ... })

Vue.use(PluginB)  // 也影响 app1!
const app2 = new Vue({ ... })

两个不同的应用实例引用的插件 PluginA和PluginB会互相影响,造成全局状态污染。

Vue 3 的工厂模式:createApp

Vue 3 彻底改变了这一设计,采用工厂模式创建应用实例:

javascript
import { createApp } from 'vue'

const app1 = createApp(App1)
app1.use(PluginA) // 只影响 app1
app1.mount('#app1')

const app2 = createApp(App2)
app2.use(PluginB) // 只影响 app2
app2.mount('#app2')

每次调用 createApp 都会返回一个全新的应用实例,每个实例都拥有独立的上下文。让我们看看源码是如何实现的。

createAppContext:多应用隔离的秘密

runtime-core/src/apiCreateApp.ts 中,有一个核心函数 createAppContext

typescript
/**
 * createAppContext 为每个 app 实例提供隔离的全局上下文:
 * - 持有 app 自身引用,方便 devtools / renderer 在任何地方取到当前 app。
 * - config/mixins/components/directives/provides 等集合都在此维护,确保同一个 app 内共享、不同 app 互不干扰。
 * - optionsCache/propsCache/emitsCache 则缓存编译/归一化后的选项,加速多次渲染。
 */
export 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(),
  }
}

这个 AppContext 对象包含了一个应用需要的所有全局状态:

  • config:应用级配置对象,包含错误处理、性能开关、全局属性等
  • mixins:应用级混入数组
  • components:全局组件注册表
  • directives:全局指令注册表
  • provides:应用级依赖注入容器
  • optionsCache/propsCache/emitsCache:各种缓存,用于优化性能

每个应用实例都会创建一个独立的 AppContext,通过闭包机制实现隔离。这就是 Vue 3 多应用隔离的核心原理。

依赖注入与跨平台能力:IoC 控制反转

分层架构:runtime-core 与 runtime-dom

Vue 3 的一个重要设计目标是跨平台。为了实现这一点,Vue 将渲染器分为两层:

runtime-dom (平台特定层)
    ↓ 注入
runtime-core (平台无关层)
  • runtime-core:包含平台无关的核心渲染逻辑(Diff 算法、组件系统、生命周期等)
  • runtime-dom:包含浏览器 DOM 特定的操作(createElement、patchProp 等)

这种分层的好处是,只要实现一套符合 RendererOptions 接口的平台操作,就可以在任何环境中运行 Vue:

  • 浏览器 DOM:runtime-dom
  • Canvas:实现 Canvas 版本的 nodeOps
  • 小程序:实现小程序版本的 nodeOps
  • 原生渲染:实现 Native 版本的 nodeOps

nodeOps:DOM 操作的适配层

runtime-dom/src/nodeOps.ts 中,定义了所有与 DOM 相关的原子操作:

typescript
/**
 * nodeOps 是 runtime-core 与真实 DOM 之间的宿主适配层:
 * - runtime-core/createRenderer 仅依赖这些原子操作,从而能在不同平台(DOM、Weex、自定义渲染器)复用同一套 diff/patch 算法。
 * - 每个方法都尽量贴近浏览器原生 API,既保证性能,也让其它宿主实现时有明确的语义参考。
 */
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  // insert 统一走 insertBefore:无锚点时等同 appendChild,有锚点则精确插入,服务于最小移动策略
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

  // remove 前先获取 parentNode,防止节点已脱离文档树时报错
  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },

  // createElement 需要照顾命名空间、自定义元素 is 属性以及 select[multiple] 特例
  createElement: (tag, namespace, is, props): Element => {
    const el =
      namespace === 'svg'
        ? doc.createElementNS(svgNS, tag)
        : namespace === 'mathml'
        ? doc.createElementNS(mathmlNS, tag)
        : is
        ? doc.createElement(tag, { is })
        : doc.createElement(tag)

    if (tag === 'select' && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
    }

    return el
  },

  createText: text => doc.createTextNode(text),
  createComment: text => doc.createComment(text),
  setText: (node, text) => {
    node.nodeValue = text
  },
  setElementText: (el, text) => {
    el.textContent = text
  },
  parentNode: node => node.parentNode as Element | null,
  nextSibling: node => node.nextSibling,
  querySelector: selector => doc.querySelector(selector),
  setScopeId(el, id) {
    el.setAttribute(id, '')
  },

  // ... 其他操作
}

这些操作看起来简单,但它们是 runtime-core 与平台解耦的关键。runtime-core 从来不会直接调用 document.createElement,而是调用注入的 hostCreateElement

ensureRenderer:延迟创建与依赖注入

runtime-dom/src/index.ts 中,有一个关键的函数 ensureRenderer

typescript
const rendererOptions = /*@__PURE__*/ extend({ patchProp }, nodeOps)

// 延迟创建 renderer,使核心渲染逻辑可被 Tree Shaking,在仅使用响应式工具时不被打包
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

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

这里有两个重要的设计:

  1. 延迟创建:只有在真正需要渲染时才创建 renderer,如果应用只使用响应式 API(refreactive),渲染器的代码可以被 Tree Shaking 掉
  2. 依赖注入:将 rendererOptions(包含 nodeOpspatchProp)注入到 createRenderer

这就是经典的**控制反转(IoC)**模式:runtime-core 不依赖具体的 DOM 实现,而是依赖抽象的 RendererOptions 接口。

createApp 的完整调用链路

从 runtime-dom 到 runtime-core

让我们跟踪一下 createApp 的完整调用链:

typescript
// 1. 用户调用 (runtime-dom)
const app = createApp(App)

// 2. runtime-dom/src/index.ts
export const createApp = ((...args) => {
  // 确保 renderer 已创建,并调用其 createApp 方法
  const app = ensureRenderer().createApp(...args)

  // 记录原始的 mount 方法
  const { mount } = app

  // 重写 mount 方法(稍后详述)
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // ... DOM 平台特定的逻辑
  }

  return app
}) as CreateAppFunction<Element>

// 3. runtime-core/src/renderer.ts
function baseCreateRenderer(options: RendererOptions, ...): any {
  // 解构出所有的宿主操作
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    // ...
  } = options

  // 返回一个包含 render 和 createApp 的对象
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

// 4. runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 创建独立的 AppContext
    const context = createAppContext()

    // 创建 app 实例
    const app: App = {
      _uid: uid++,
      _component: rootComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      use() { ... },
      mixin() { ... },
      component() { ... },
      directive() { ... },
      mount() { ... },
      unmount() { ... },
      provide() { ... },
      // ...
    }

    return app
  }
}

调用链路总结:

这个链路展示了 Vue 3 的适配器模式

  • runtime-dom 作为适配器,将 DOM 操作注入到 runtime-core
  • runtime-core 提供通用的应用创建逻辑
  • 返回的 app 实例被 runtime-dom 进一步增强(重写 mount)

createAppAPI:应用实例的工厂

createAppAPI 是一个高阶函数,它接收 renderhydrate 函数,返回一个 createApp 工厂:

typescript
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 统一组件入口类型:Options API 组件需转成全新的对象,避免多实例之间共享引用
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }

    // 每个 app 单独持有上下文
    const context = createAppContext()
    const installedPlugins = new WeakSet()
    const pluginCleanupFns: Array<() => any> = []

    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) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },

      // 插件安装:同一 app 内幂等执行
      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)
        }
        return app
      },

      // 全局 mixin
      mixin(mixin: ComponentOptions) {
        if (!context.mixins.includes(mixin)) {
          context.mixins.push(mixin)
        }
        return app
      },

      // 组件注册/查询
      component(name: string, component?: Component): any {
        if (__DEV__) {
          validateComponentName(name, context.config)
        }
        if (!component) {
          return context.components[name]
        }
        context.components[name] = component
        return app
      },

      // mount 方法(稍后详述)
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        namespace?: boolean | ElementNamespace
      ): any {
        if (!isMounted) {
          // 创建根 VNode
          const vnode = createVNode(rootComponent, rootProps)
          vnode.appContext = context

          // 执行渲染或激活
          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            render(vnode, rootContainer, namespace)
          }

          isMounted = true
          app._container = rootContainer
          app._instance = vnode.component

          return getComponentPublicInstance(vnode.component!)
        }
      },

      // ... 其他方法
    })

    return app
  }
}

关键点:

  1. 闭包捕获 render 和 hydratemount 方法内部调用的 render 就是从外层闭包捕获的,它已经携带了平台特定的 nodeOps
  2. 链式调用:所有注册方法(usemixincomponent 等)都返回 app 实例,支持链式调用
  3. 幂等性保护:插件和 mixin 都有重复检查,避免多次注册

mount 重写的秘密:装饰器模式

为什么要重写 mount?

在 runtime-dom 中,你会发现 app.mount 被重写了:

typescript
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  // 记录 runtime-core createApp 返回的原始 mount
  const { mount } = app

  // 重写 mount
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // DOM 平台特定的逻辑
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }

    // 清空容器
    if (container.nodeType === 1) {
      container.textContent = ''
    }

    // 调用原始 mount
    const proxy = mount(container, false, resolveRootNamespace(container))

    // 移除 v-cloak,添加 data-v-app
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }

    return proxy
  }

  return app
}) as CreateAppFunction<Element>

这是经典的装饰器模式

  • 保存原始的 mount 方法
  • 在新的 mount 中添加平台特定的逻辑
  • 最后调用原始的 mount

runtime-dom 的 mount 做了什么?

让我们逐步分析 DOM 层的 mount 逻辑:

第一步:normalizeContainer - 统一容器类型

typescript
function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | ShadowRoot | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    if (__DEV__ && !res) {
      warn(
        `Failed to mount app: mount target selector "${container}" returned null.`
      )
    }
    return res
  }
  return container as any
}

这个函数让用户既可以传 DOM 元素,也可以传选择器字符串:

javascript
app.mount('#app') // 选择器
app.mount(document.querySelector('#app')) // DOM 元素

第二步:兜底模板 - 使用 innerHTML 作为模板

typescript
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
  // ⚠️ 非安全路径:内联 DOM 模板可能包含可执行的 JS 表达式
  component.template = container.innerHTML
}

如果组件既没有 render 函数,也没有 template 选项,就使用容器的 innerHTML 作为模板。

注意:这是一个潜在的 XSS 风险点。Vue 官方建议生产环境使用预编译模板,避免运行时编译。

第三步:清空容器 - 避免旧 DOM 混淆

typescript
if (container.nodeType === 1) {
  container.textContent = ''
}

在挂载前清空容器,避免旧的 DOM 节点与 Vue 渲染的节点混淆。这是一个重要的细节,确保挂载的干净性。

第四步:调用原始 mount

typescript
const proxy = mount(container, false, resolveRootNamespace(container))

调用 runtime-core 的原始 mount 方法,传入:

  • container:容器元素
  • false:不是 hydrate 模式(SSR 客户端激活)
  • resolveRootNamespace(container):解析命名空间(SVG/MathML)

第五步:DOM 平台的收尾工作

typescript
if (container instanceof Element) {
  container.removeAttribute('v-cloak')
  container.setAttribute('data-v-app', '')
}
  • 移除 v-cloak 指令(用于防止未编译模板闪烁)
  • 添加 data-v-app 属性(用于调试和样式隔离)

runtime-core 的 mount 做了什么?

runtime-core 的 mount 方法才是真正的渲染入口:

typescript
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  namespace?: boolean | ElementNamespace,
): any {
  if (!isMounted) {
    // 创建根 VNode
    const vnode = createVNode(rootComponent, rootProps)
    vnode.appContext = context

    // 根据是否 hydrate 选择不同的渲染路径
    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer, namespace)
    }

    isMounted = true
    app._container = rootContainer
    app._instance = vnode.component

    // 返回组件公开实例
    return getComponentPublicInstance(vnode.component!)
  }
}

关键步骤:

  1. 创建根 VNode:将根组件和 props 转换为 VNode
  2. 绑定 AppContext:确保整个组件树都能访问应用上下文
  3. 执行渲染:调用 render 函数(携带平台操作的 patch 函数)
  4. 标记已挂载:防止重复挂载
  5. 返回公开实例:用户拿到的是包装后的组件实例(有 $el$refs 等)

架构模式总结

通过分析 Vue 3 的 createApp 实现,我们可以总结出以下几个核心的架构模式:

工厂模式(Factory Pattern)

createApp 本身就是一个工厂函数,每次调用都创建一个全新的应用实例:

typescript
export function createApp(rootComponent, rootProps = null) {
  const context = createAppContext()  // 独立的上下文
  const app: App = { ... }            // 独立的实例
  return app
}

优势

  • 避免全局状态污染
  • 支持多应用隔离
  • 便于测试和维护

控制反转(IoC / Dependency Injection)

runtime-core 不依赖具体的 DOM 实现,而是依赖抽象的 RendererOptions 接口:

typescript
function baseCreateRenderer(options: RendererOptions) {
  const {
    insert: hostInsert,
    remove: hostRemove,
    createElement: hostCreateElement,
    // ...
  } = options

  // 使用注入的操作,而不是直接调用 DOM API
  const patch = (...) => {
    hostInsert(el, parent, anchor)
  }
}

优势

  • 平台无关的核心逻辑
  • 可以轻松支持新平台
  • 代码复用性强

装饰器模式(Decorator Pattern)

runtime-dom 重写了 app.mount,在原有逻辑基础上添加平台特定的行为:

typescript
const { mount } = app

app.mount = (container) => {
  // 平台特定的前置处理
  normalizeContainer(container)

  // 调用原始逻辑
  const proxy = mount(container, ...)

  // 平台特定的后置处理
  container.removeAttribute('v-cloak')

  return proxy
}

优势

  • 不修改核心逻辑
  • 灵活扩展功能
  • 符合开闭原则

适配器模式(Adapter Pattern)

nodeOps 将浏览器 DOM API 适配成渲染器需要的接口:

typescript
export const nodeOps = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },
  remove: child => {
    const parent = child.parentNode
    if (parent) parent.removeChild(child)
  },
  // ...
}

优势

  • 统一接口规范
  • 隔离平台差异
  • 便于单元测试(可以 mock nodeOps)

策略模式(Strategy Pattern)

渲染器根据 VNode 的类型(通过 shapeFlag)选择不同的处理策略:

typescript
const patch = (n1, n2, container, ...) => {
  const { type, shapeFlag } = n2

  switch (type) {
    case Text:
      processText(n1, n2, container)
      break
    case Comment:
      processComment(n1, n2, container)
      break
    case Fragment:
      processFragment(n1, n2, container)
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container)
      }
  }
}

优势

  • 代码清晰易维护
  • 易于扩展新的节点类型
  • 性能优化(编译期确定类型)

总结

通过深入分析 Vue 3 的 createApp 实现,我们就可以知道createApp 到底干了啥?

  1. 创建隔离的应用上下文:通过 createAppContext 为每个应用创建独立的全局状态
  2. 实现依赖注入:通过 ensureRenderer 将平台操作注入到核心渲染器
  3. 装饰 mount 方法:在 runtime-dom 层添加平台特定的逻辑
  4. 初始化渲染管线:创建 render 函数,准备 patch 算法
  5. 返回应用实例:提供 usemixincomponent 等 API

而这种架构设计的精髓给我们打来了:

  • 工厂模式解决了 Vue 2 的全局污染问题
  • 控制反转实现了真正的跨平台能力
  • 装饰器模式优雅地分离了通用逻辑和平台逻辑
  • 位掩码优化让类型判断和特征检查性能达到极致
  • Block Tree机制让 Diff 算法跳过大量静态节点

Vue 3 的架构设计不仅仅是为了实现功能,更是为了可扩展性可维护性性能的极致平衡。这些设计模式和优化策略,值得我们在日常开发中借鉴和应用。

版本演进:createApp 的优化历程

版本变化说明
Vue 2.xnew Vue()全局构造函数,共享配置
Vue 3.0createApp()工厂模式,应用隔离
Vue 3.0AppContext独立的应用上下文
Vue 3.2effectScope副作用作用域管理
Vue 3.3app.runWithContext()在应用上下文中执行代码
Vue 3.5插件清理函数app.use() 返回的插件可以提供清理函数

Vue 3.3 的 runWithContext

ts
// Vue 3.3 新增:在应用上下文中执行代码
app.runWithContext(() => {
  // 在这里可以使用 inject() 获取应用级 provide 的值
  const theme = inject('theme')
})

这个 API 解决了在组件外部(如路由守卫、Pinia actions)访问应用级依赖注入的问题。

Vue 3.5 的插件清理

ts
// Vue 3.5:插件可以返回清理函数
app.use({
  install(app) {
    // 安装逻辑
    return () => {
      // 清理逻辑(app.unmount 时调用)
    }
  }
})

从 Vue 2 到 Vue 3 的架构演进

Vue 2 架构:
  Vue 构造函数(全局单例)
    ├─ Vue.use() → 修改原型
    ├─ Vue.mixin() → 修改原型
    └─ new Vue() → 共享配置

Vue 3 架构:
  createApp() 工厂函数
    ├─ 独立的 AppContext
    ├─ 独立的 renderer 实例
    └─ 独立的 mount/unmount 生命周期

随堂测试

  1. Vue 3 使用工厂函数 createApp() 替代 Vue 2 的 new Vue(),除了解决全局污染问题,这种设计对 Tree Shaking 有什么影响?如果一个项目只使用响应式 API 而不渲染任何组件,打包体积会有什么变化?

  2. ensureRenderer() 采用延迟创建的方式,只在首次调用时才实例化渲染器。这种惰性初始化模式在什么场景下特别有价值?如果改成立即创建会有什么问题?

  3. runtime-dom 重写 app.mount() 时,为什么要先保存原始方法再调用,而不是直接在 runtime-core 中处理 DOM 相关逻辑?这种装饰器模式与继承相比有什么优劣?

  4. AppContext 中的 optionsCachepropsCacheemitsCache 都使用 WeakMap 存储。为什么选择 WeakMap 而不是普通 Map?在组件频繁创建销毁的场景下,这对内存管理有什么意义?

参考文件

  • /packages/runtime-dom/src/index.ts - DOM 平台适配层
  • /packages/runtime-core/src/apiCreateApp.ts - 应用实例创建逻辑
  • /packages/runtime-core/src/renderer.ts - 核心渲染器实现
  • /packages/runtime-dom/src/nodeOps.ts - DOM 操作适配
  • /packages/shared/src/shapeFlags.ts - 节点形状标记
  • /packages/shared/src/patchFlags.ts - 补丁标记
  • /packages/runtime-core/src/vnode.ts - VNode 数据结构定义

扫描关注微信 - 前端小卒,获取更多 Vue 3 源码解析内容