本文的目的就是从createApp 开始详细解读一下Vue3创建应用的过程。
创建应用
<body>
<script src="../../dist/vue.global.js"></script>
<div id="root">
<h1>{{text}}</h1>
</div>
<script>
const app = Vue.createApp({
data() {
return {
text: 'Hello Vue'
}
}
})
app.mount('#root')
</script>
</body>
我们通过以上代码就能创建一个最简单的vue 应用(它的功能是在浏览器窗口中展示Hello Vue 的h1标签)。
上述过程一共做了2件事情:
- 通过
createApp函数创建app对象 - 通过
app.mount方法将内容挂载到id为root的标签上
app对象的产生
app 是Vue应用的本体,它由Vue的全局api createApp 生成。接下来让我们一起来看看 createApp 的内部实现,直到 app 的创建。
createApp 函数位于vue-next/packages/runtime-dom/src/index.ts,它的类型定义为:
export type CreateAppFunction<HostElement> = (
rootComponent: Component,
rootProps?: Data | null
) => App<HostElement>
createApp 函数接收 rootComponent 根组件作为参数, rootProps 根组件的props作为可选参数。
具体实现为:
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// ... 此处逻辑省略
}
return app
}) as CreateAppFunction<Element>
createApp 内部流程如下:
- 利用
ensureRendered创建rendered渲染器,并利用rendered上的createApp方法创建app对象。 - 重写
app对象上的mount方法 - 返回
app对象(即应用实例)
创建renderer
从以上流程我们能够知道 createApp 函数的内部是通过 rendered 渲染器来创建应用实例的,所以我们需要通过 createRenderer 函数来创建渲染器。即:
// vue-next/packages/runtime-dom/src/index.ts
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
看到这里,也许有读者可能会和我对于 ensureRenderer 的实现有同样的疑惑,为什么 ensureRenderer 函数不直接返回 createRenderer 函数的调用而是像return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)) 来实现延迟创建渲染器呢?
这是因为Vue3中实现了全局api的 tree-shaking ,为了使得核心渲染逻辑能够 tree-shakable 而采用了延迟创建 rendered 。(我们是从以下注释知道的)
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
createRendered 内部调用了 createBaseRendered 来创建 rendered :
// vue-next/packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
createBaseRendered 是创建 rendered 的底层函数,该函数的内部逻辑比较复杂,后面我有时间会单开一篇来具体分析一下内部流程。此时我们只要关注该函数的返回值:
// vue-next/packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// ... 此处逻辑省略
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
ok,我们终于看到了 ensureRenderer().createApp(...args) 中 createApp 的来源。该属性的值是调用 createAppAPI 函数后的返回结果。
app应用实例来自于 createAppAPI
createAppAPI 的内部实现如下:
// vue-next/packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 创建全局上下文
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) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
use(plugin: Plugin, ...options: any[]) {
// ... 插件化
return app
},
mixin(mixin: ComponentOptions) {
// ... 全局mixin
return app
},
component(name: string, component?: Component): any {
// ... 全局组件注册
return app
},
directive(name: string, directive?: Directive) {
// ... 全局指令注册
return app
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// ... 应用挂载
},
unmount() {
// ... 应用卸载
},
provide(key, value) {
// ... 依赖注入
return app
}
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
从上述代码我们可以看到 createAppAPI 函数 返回 createApp 函数,而返回的 createApp 函数执行后就可以得到 app 应用实例,其中 app 对象进行了一系列初始化操作,并通过 createAppContext() 可以得到应用全局上下文 context,后面应用中所有的组件都可以进行访问。
重写 app 对象上的 mount 方法
分析完渲染器的创建过程后,我们继续移步到 全局API createApp 内部流程的下一步:重写 app 对象上的 mount 方法。
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector) // 同时支持字符串和DOM对象
if (!container) return
const component = app._component
// 若根组件非函数对象且未设置render和template属性,则使用容器的innerHTML作为模板的内容
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
container.innerHTML = '' // 在挂载前清空容器内容
const proxy = mount(container) // 执行挂载操作
if (container instanceof Element) {
container.removeAttribute('v-cloak') // 避免在网络不好或加载数据过大的情况下,页面渲染的过程中会出现Mustache标签
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
在 app.mount 方法内部,当设置好根组件的相关信息之后,就会调用 app 对象原始的 mount 方法执行挂载操作。
那么为什么要重写 app.mount 方法呢?原因是为了支持跨平台,在 runtime-dom 包中定义的 app.mount 方法,都是与 Web 平台有关的方法。
总结
ok,以上就是 createApp 内部的具体流程,我们会发现 Vue3 的内部实现虽然十分复杂,但是功能模块的分离非常清晰,且给开发者暴露的api 是如此简洁优雅,以致我们用 const app = createApp({}) 就可以创建一个应用实例。