💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4)
1、前言
在Vue3中,我们挂载实例采用:createApp(App).mount("#app");请问这个过程发生了什么呢?
javacript
import { createApp } from "vue";//引入的runtime-dom
import App from "./App.vue";
// 今天主要分析createApp这个函数
createApp(App).mount("#app");
2、createApp函数
createApp函数源码在core/packages/runtime-dom/src/index.ts目录下:****
核心代码如下:
javacript
export const createApp = ((...args) => {
// 创建app对象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重写mount方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 获取<div id="root"></div>的DOM对象(这个函数处理了字符串和真实DOM两种情况)
const container = normalizeContainer(containerOrSelector)
const component = app._component
// 如果组件对象没有定义 render 函数和 template 模板(首次初始化的时候),则取容器的 innerHTML 作为组件模板内容
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
// 将根节点下的HTML内容添加到组件的template上
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
// 真正的挂载(挂载容器,是否是ssr,是否是svg元素)
const proxy = mount(container, false, container instanceof SVGElement)
// 返回渲染器的所有方法的集合
return proxy
}
return app
}) as CreateAppFunction<Element>
核心代码做了两件事情:
- 调用ensureRenderer函数获取渲染器,然后执行渲染器的createApp方法常见了app实例;
- 获取app实例上的mount方法,并对重写了mount方法;
接着,我们看看深入ensureRenderer,看它做了什么事情。
2.1 ensureRender函数
我们进入ensureRender函数后,它的实现如下:
jsx
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-dom/src/index.ts#L41-L46
function ensureRenderer() {
// 最终返回了一个对象 {render,createApp}
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
这个函数的返回值,实际上是返回了一个render渲染器,返回之前,首先判断了渲染器是否存在,如果不存在,则调用createRenderer函数创建了一个渲染器。我们接着深入createRenderer函数看它的实现。
2.2 createRenderer函数
我们进入createRenderer函数后,它的实现如下:
javacript
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/renderer.ts#L296-L301
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
// 创建基础的渲染器
return baseCreateRenderer<HostNode, HostElement>(options)
}
createRenderer根据传入的options,调用baseCreateRenderer创建了一个基础的渲染器。
2.3 baseCreateRenderer函数
baseCreateRenderer函数实现很长,我们先只关注它的返回值:
jsx
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/renderer.ts#L2354-L2359
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
从上面的代码中,我们发现它返回了一个对象,里面有render、hydrate、createApp属性,而createApp是根据createAppAPI函数实现的,我们继续进入createAppAPI函数里面去看它的实现。
2.4 createAppAPI函数
baseCreateRenderer函数实现很长,我们先只关注它做了哪些事情:
javacript
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/apiCreateApp.ts#L199-L416
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
// 返回开发者使用的app工厂函数
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 1.创建上下文
const context = createAppContext()
// 2.声明一个不可重复的插件容器
const installedPlugins = new Set()
// 3.初始化isMounted的状态是false
let isMounted = false
// 4.应用程序实例:创建app,并将其添加到context对象的app属性上
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
// ...
},
set config(v) {
// ....
},
use(plugin: Plugin, ...options: any[]) {
// ...
},
mixin(mixin: ComponentOptions) {
// ...
},
component(name: string, component?: Component): any {
// ...
},
directive(name: string, directive?: Directive) {
// ...
},
// eg. app.mount('#app'),核心渲染逻辑,将vnode转换成真实的DOM
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// ...
},
unmount() {
// ...
},
provide(key, value) {
// ...
},
runWithContext(fn) {
// ...
}
})
// 5.考虑兼容的属性
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
baseCreateRenderer实际上返回了createApp工厂函数,该函数其实就是开发者在页面上调用的createApp方法。在该工厂函数中中创建了app实例,并将添加到context对象的app属性上,最后将app实例返回。
2.5 createApp函数调用流程
从createApp函数一路分析,最终的调用链为:createApp() -> ensureRenderer() -> createRenderer() => renderer -> renderer.createApp()。
3、挂载流程
3.1 mount函数
页面上执行完createApp函数后,就会调用mount方法去挂载实例。我们首先来看mount重写的实现
javacript
const { mount } = app
// 重写mount方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 获取<div id="root"></div>的DOM对象(这个函数处理了字符串和真实DOM两种情况)
const container = normalizeContainer(containerOrSelector)
if (!container) return
// app是一个包含component\config\directive\mixin\mount\provide等属性的实例对象,该实例对象提供了一个应用上下文,实例对象上的方法可以链式调用
const component = app._component
// 如果组件对象没有定义 render 函数和 template 模板(首次初始化的时候),则取容器的 innerHTML 作为组件模板内容
if (!isFunction(component) && !component.render && !component.template) {
// 将根节点下的HTML内容添加到组件的template上
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
// 真正的挂载(挂载容器,是否是ssr,是否是svg元素)
const proxy = mount(container, false, container instanceof SVGElement)
// 返回渲染器的所有方法的集合
return proxy
}
请问为什么这里要重新mount方法呢?这是因为不同平台的mount方法所做的工作不一样,底层mount是通用实现,这里重写是为了在web环境进行dom节点的挂载。我们继续看底层mount方法的实现。
javacript
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/apiCreateApp.ts#L319-372
// eg. app.mount('#app'),核心渲染逻辑,将vnode转换成真实的DOM
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 判断当前返回的app是否已经调用过mount方法
if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
// 创建根组件对应的vnode,即虚拟DOM(根据编译后的.vue文件生成对应的虚拟节点)
const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
// 将上下文信息挂载到根组件节点的 appContext 属性上
vnode.appContext = context
// HMR root reload
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 {
// 利用外部传入的渲染器渲染vnode(将vnode挂载到用户传入的container中),即将VNode转换为真实的DOM
render(vnode, rootContainer, isSVG)
}
isMounted = true
// 建立app与DOM的关联
app._container = rootContainer
// 根容器上设置一个特殊标记 __vue_app__,用于判断一个 DOM 元素上是否已经挂载了 Vue 应用实例
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
// 如果是开发环境或者开启了生产环境下的开发工具支持,还会将应用实例的 _instance 属性设置为根 VNode 对应的组件实例,然后调用 devtoolsInitApp 函数,将应用实例注册到开发工具中。这样,开发者就可以在开发工具中查看应用的状态和行为,方便调试和排查问题
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
},
该函数首先createVNode函数创建根组件对应的虚拟VNode,然后判断是否是服务端渲染:
- 服务端渲染:调用hydrate方法将VNode转换成真实的DOM;
- 非服务端渲染:调用外部传入的render方法,将VNode转换成真实的DOM,然后渲染出真正的html;
3.1 render函数
javacript
// 如果第一个参数是null,则执行销毁组件的逻辑,否则执行patch函数来创建或者更新组件的逻辑
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)
}
flushPreFlushCbs()
flushPostFlushCbs()
container._vnode = vnode
}
该函数实现比较简单,首先判断vnode节点是否存在,如果为null,则执行卸载组件逻辑,否则执行patch更新(即Diff过程)。
4、createApp整体流程图
结合createApp创建实例和mount挂载流程后,Vue3 实例整个挂载流程如下:
5、参考资料
[1] Vue官网
[2] Vuejs设计与实现