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
方法的定义在 createApp
API 函数返回的开发者使用的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
过程),并建立更新机制。