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 文件中,下面仅贴出关键代码:
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 只做了两件事情:
- 调用
ensureRenderer函数获取渲染器,然后执行渲染器的createApp方法创建app应用实例。
js
// 获取渲染器,并执行渲染器的 createApp 方法,创建 app 应用实例
const app = ensureRenderer().createApp(...args)
- 获取
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
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方法,对其进行扩展。
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 方法的定义在 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做了两件事情:
- 调用
ensureRenderer函数获取渲染器,然后执行渲染器的createApp方法创建app应用实例。 - 在创建完
app应用程序实例后,会取出app上的mount方法,对其进行扩展。在mount方法中,创建根节点的VNode,进入patch过程 (即Diff过程),并建立更新机制。