创建 Vue 应用实例,是一个项目的开始,这篇文章看看 createApp 内部都发生了什么。当然如果你对 Vue3 响应式系统感兴趣,也可以先看看这两篇文章:
Vue3源码 | 深入理解响应式系统上篇-reactive
Vue3源码 | 深入理解响应式系统下篇-effect
VUE3 使用
这里看下 Vue3 是如何进行应用初始化。
// vue3 应用初始化
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
// 组件渲染和未捕获错误配置的处理程序
app.config.errorHandler = (err, vm, info) => {}
// 添加全局属性
app.config.globalProperties.$http = () => {} // 这里相当于挂载到Vue2的 Vue.prototype
// 指定一种方法识别Vue之外定义的自定义元素
app.config.isCustomElement = tag => tag.startsWith('ion-')
// 注册组件
app.component('my-component', {})
// 检索组件
const MyComponent = app.component('my-component')
// 注册指令
app.directive('my-directive',{})
// 设置一个可以注入到应用程序内所有组件中的值。组件应使用inject来接收提供的值。
app.provide('user', 'administrator')
// 卸载应用程序
app.unmount()
// 安装vue插件
import MyPlugin from './plugins/MyPlugin'
app.use(MyPlugin)
...
复制代码
Vue3 跟 Vue2 初始化区别不大,都是创建应用实例,然后挂载到 DOM 节点上。这里也可以看出,Vue 是通过 JS 渲染页面,跟传统页面 DOM 直出的方式是不一样。DOM 直出,简单说是,请求后返回的 HTML 页面是最终的呈现效果。
createApp 流程
createApp
createApp 用于创建应用实例,下面看看内部代码实现:
export const createApp = ((...args) => {
// 创建app实例
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重写mount方法
app.mount = (containerOrSelector: Element | string): any => {
// 标准化容器
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
// 组件不存在render函数和模板template,则使用container的innerHTML做为组件模板
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 挂载前,清空容器的内容
container.innerHTML = ''
// 挂载容器
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
return app
}) as CreateAppFunction<Element>
复制代码
通过上面源码解析,我们可以看出 createApp 主要是干了两件事:
创建 app 实例,并返回该实例
重写 mount 方法
看完会存在两个主要疑问,ensureRenderer
是干啥用的?为什么要重写 mount
方法,而不直接使用呢?
ensureRenderer
用于创建渲染器,渲染器核心代码处于 runtime-core/src/renderer.ts
文件。
baseCreateRenderer// 可以看出render存在2种类型的渲染器
let renderer: Renderer<Element> | HydrationRenderer
// 延迟创建渲染器,当用户只是使用reactive响应库,可以做tree-shaking优化
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
// HydrationRenderer渲染器,也是只在调用的时候创建,方便做tree-shaking优化
export function createHydrationRenderer(
options: RendererOptions<Node, Element>
) {
return baseCreateRenderer(options, createHydrationFunctions)
}
复制代码
从分析可以看出:
存在 2 种类型的渲染器,它们都是基于 baseCreateRenderer
函数创建,此函数存在重载。
渲染器都是通过延迟创建,方便不使用的时候做 tree-shaking
。
baseCreateRenderer
这个函数是个大家伙,1800 行左右的代码,包含了组件渲染的核心逻辑。这里只抽离挂载过程相关逻辑,涉及组件更新等其他渲染逻辑,留到后面再深入研究。
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 其他关于组件渲染逻辑以及hydrate相关这里省略
// 渲染函数
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
// 虚拟节点不存在,则销毁
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 虚拟节点存在,则更新或创建
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
// 缓存虚拟节点数据,作为已完成渲染的标识
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
复制代码
可以看出 baseCreateRenderer
主要实现了:
实现了组件渲染的创建、更新、卸载等核心逻辑(后续解读)
返回渲染函数,以及创建应用实例方法,当然还有 hydrate
。
createAppAPI
组件渲染核心部分后面看,这里看看创建实例的 API 实现。
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
// createApp接收2个参数,根组件和根组件的属性
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
// 重写上下文对象的app属性
const app: App = (context.app = {
_component: rootComponent as Component,
_props: rootProps,
_container: null,
_context: context,
version,
// 暴露的应用实例的配置
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
// 插件的安装,可以看出,插件如果是对象,必须有install方法;如果是函数,则默认是安装方法
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)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
}
}
return app
},
component(name: string, component?: PublicAPIComponent): any {
if (__DEV__) {
validateComponentName(name, context.config)
}
// 组件存在,则返回
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
// 不存在就注册
context.components[name] = component
return app
},
directive(name: string, directive?: Directive) {
if (__DEV__) {
validateDirectiveName(name)
}
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
// 根据根组件创建虚拟节点
const vnode = createVNode(rootComponent as Component, rootProps)
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer)
}
}
render(vnode, rootContainer)
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
}
},
unmount() {
if (isMounted) {
render(null, app._container)
devtoolsUnmountApp(app)
}
},
provide(key, value) {
context.provides[key as string] = value
return app
}
})
return app
}
}
复制代码
从上面的代码,我们可以了解到 createAppAPI 主要实现了:
创建定义一个实例上下文 context,包含属性和方法
重写扩展 context.app 方法,实现用户可以对上下文相关属性的自定义操作,也就是应用实例暴露的 api 实现,比如自定义指令、混入 mixin、组件等提供用户自定义实现。
根据根组件和属性在 mount
方法中完成虚拟节点 vNode
的转换,并通过 render
喊完成渲染,关于渲染函数在 baseCreateRender
已经说过。
总结
至此分析了 createApp 大致的流程,内部更细致的实现,后续再根据内容深入分析,这里再总结下整个过程。
执行 createApp
首先会创建渲染器,这里要注意的是存在 2 种渲染器类型,并且它们都是通过延迟创建的,主要目的是当用户只引用 reactive 响应式框架的时候,方便进行 tree-shaking 优化。且两种渲染器都是基于 baseCreateRender
方法来实现。
baseCreateRender
函数执行后会返回 render
渲染函数和 createApp
方法,其中 render
函数是组件创建、更新和卸载的主要核心逻辑实现。createApp 则用于创建应用实例,进行应用实例的初始化。
createAppAPI 用于生成默认的应用上下文 context
,这里定义了应用实例具备的属性和方法,并通过重写扩展 context.app
属性,让用户能够进行对上下文的自定义操作,比如自定义组件、指令、mixin、插件安装等一系列操作。并存在 mount 方法完成将根组件转为虚拟节点 vNode
,并通过render
函数完成对 vNode
的渲染。
下一篇,我们将立足这篇文章,接着深入 render 函数内部,先看看 vNode 是什么,再瞧瞧 DOM Diff 过程。
评论