Appearance
Vue 3 createApp 到底干了啥?深度剖析架构设计与渲染初始化
如果你用过 Vue 2,一定对 new Vue() 这个经典的初始化方式印象深刻。但到了 Vue 3,一切都变了——我们现在用的是 createApp()。这不仅仅是 API 的改变,更是 Vue 架构层面的一次深刻重构。
很多人只知道怎么用 createApp,却不清楚它背后做了什么。为什么要从 Class 实例化改成工厂函数?为什么要分 runtime-dom 和 runtime-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({ ... })这种设计存在严重的问题:
- 全局状态污染:
Vue.use、Vue.mixin等操作会修改 Vue 构造函数的原型,导致所有实例共享这些全局配置 - 多应用隔离困难:在同一页面中创建多个独立的 Vue 应用时,它们之间会相互影响
- 测试不友好:单元测试时,全局状态会在测试用例之间泄漏,需要手动清理
举个实际例子,假设你在一个页面中需要同时渲染两个独立的应用:
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))
)
}这里有两个重要的设计:
- 延迟创建:只有在真正需要渲染时才创建 renderer,如果应用只使用响应式 API(
ref、reactive),渲染器的代码可以被 Tree Shaking 掉 - 依赖注入:将
rendererOptions(包含nodeOps和patchProp)注入到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 是一个高阶函数,它接收 render 和 hydrate 函数,返回一个 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
}
}关键点:
- 闭包捕获 render 和 hydrate:
mount方法内部调用的render就是从外层闭包捕获的,它已经携带了平台特定的 nodeOps - 链式调用:所有注册方法(
use、mixin、component等)都返回app实例,支持链式调用 - 幂等性保护:插件和 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!)
}
}关键步骤:
- 创建根 VNode:将根组件和 props 转换为 VNode
- 绑定 AppContext:确保整个组件树都能访问应用上下文
- 执行渲染:调用
render函数(携带平台操作的 patch 函数) - 标记已挂载:防止重复挂载
- 返回公开实例:用户拿到的是包装后的组件实例(有
$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 到底干了啥?
- 创建隔离的应用上下文:通过
createAppContext为每个应用创建独立的全局状态 - 实现依赖注入:通过
ensureRenderer将平台操作注入到核心渲染器 - 装饰 mount 方法:在 runtime-dom 层添加平台特定的逻辑
- 初始化渲染管线:创建
render函数,准备 patch 算法 - 返回应用实例:提供
use、mixin、component等 API
而这种架构设计的精髓给我们打来了:
- 工厂模式解决了 Vue 2 的全局污染问题
- 控制反转实现了真正的跨平台能力
- 装饰器模式优雅地分离了通用逻辑和平台逻辑
- 位掩码优化让类型判断和特征检查性能达到极致
- Block Tree机制让 Diff 算法跳过大量静态节点
Vue 3 的架构设计不仅仅是为了实现功能,更是为了可扩展性、可维护性和性能的极致平衡。这些设计模式和优化策略,值得我们在日常开发中借鉴和应用。
版本演进:createApp 的优化历程
| 版本 | 变化 | 说明 |
|---|---|---|
| Vue 2.x | new Vue() | 全局构造函数,共享配置 |
| Vue 3.0 | createApp() | 工厂模式,应用隔离 |
| Vue 3.0 | AppContext | 独立的应用上下文 |
| Vue 3.2 | effectScope | 副作用作用域管理 |
| Vue 3.3 | app.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 生命周期随堂测试
Vue 3 使用工厂函数
createApp()替代 Vue 2 的new Vue(),除了解决全局污染问题,这种设计对 Tree Shaking 有什么影响?如果一个项目只使用响应式 API 而不渲染任何组件,打包体积会有什么变化?ensureRenderer()采用延迟创建的方式,只在首次调用时才实例化渲染器。这种惰性初始化模式在什么场景下特别有价值?如果改成立即创建会有什么问题?runtime-dom 重写
app.mount()时,为什么要先保存原始方法再调用,而不是直接在 runtime-core 中处理 DOM 相关逻辑?这种装饰器模式与继承相比有什么优劣?AppContext中的optionsCache、propsCache、emitsCache都使用 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 数据结构定义
