各位靓仔靓女,晚上好!今天咱们来聊聊 Vue 3 的“启动按钮”—— createApp
函数。 别看它名字简单,内部可是乾坤满满。 它就像一个总指挥,负责初始化应用上下文,然后把这个上下文交给渲染器,最终才能把咱们写的 Vue 组件变成屏幕上能看到的界面。
今天我将以讲座的模式,深入剖析 createApp
的源码逻辑,保证你听完之后,也能像我一样,对 Vue 3 的启动流程了如指掌。 准备好了吗? Let’s go!
一、createApp
函数: 门面担当与内部构造
首先,我们来看看 createApp
函数的定义。 在 Vue 3 源码中,它通常位于 packages/vue/src/createApp.ts
文件中。 简化后的代码结构如下:
import { createComponentApp } from './apiCreateComponent'
import { createHydrationFunctions } from './hydration'
export function createApp(...args: any[]): any {
const app = createComponentApp(...args)
if (__FEATURE_SUSPENSE__) {
injectHydrationFunctions(app)
}
return app
}
可以看到,createApp
实际上只是一个门面函数,它内部调用了 createComponentApp
函数来创建应用实例,然后根据特性标志(__FEATURE_SUSPENSE__
)注入水合相关的功能(如果开启了服务端渲染)。
所以,真正的核心逻辑都在 createComponentApp
函数里面。 让我们深入到 createComponentApp
一探究竟。
二、createComponentApp
: 应用上下文的创建者
createComponentApp
函数负责创建应用上下文 (app context) ,并返回一个包含各种 API 的应用实例。 简化后的代码如下:
import {
createAppAPI,
CreateAppFunction,
AppContext
} from './apiCreateApp'
import {
Component,
ComponentOptions
} from './component'
import {
VNode,
createVNode,
render
} from './renderer'
import {
isString,
isFunction,
isObject
} from '@vue/shared'
import {
EMPTY_OBJ
} from '@vue/shared'
export function createComponentApp(...args: any[]): any {
let rootComponent: Component
let rootProps: any = EMPTY_OBJ
if (args.length === 1) {
rootComponent = args[0]
} else {
rootComponent = args[0]
rootProps = args[1]
}
if (rootComponent && !isObject(rootComponent)) {
rootComponent = { template: rootComponent } as Component
}
const context: AppContext = createAppContext()
const app = createAppAPI(render, context)
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return;
const vnode: VNode = createVNode(rootComponent, rootProps)
vnode.appContext = context
render(vnode, container)
}
return app
}
function createAppContext(): AppContext {
return {
config: {
isNativeTag: () => false,
performance: false,
globalProperties: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: Object.create(null)
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap(),
renderCache: new WeakMap(),
emitted: new Set()
}
}
function normalizeContainer(element: string | Element): Element | null {
if (isString(element)) {
const selector = element
const el = document.querySelector(selector)
if (!el) {
__DEV__ && warn(`Failed to mount app: mount target selector "${selector}" returned null.`)
return null
}
return el
}
return element
}
让我们一步一步地分析这个函数:
-
参数处理:
createComponentApp
接收不定数量的参数 (...args
),通常是根组件 (rootComponent
) 和可选的根组件 props (rootProps
)。- 根据参数的数量,将参数分别赋值给
rootComponent
和rootProps
。 - 如果
rootComponent
不是一个对象,则将其转换为一个包含template
属性的组件对象。 这样做是为了兼容字符串模板的情况。
-
创建应用上下文:
- 调用
createAppContext()
函数创建一个应用上下文对象context
。 createAppContext
返回一个包含以下属性的对象:config
: 应用配置项,例如isNativeTag
(判断是否是原生 HTML 标签),globalProperties
(全局属性),errorHandler
(错误处理函数) 等。mixins
: 全局混入。components
: 全局注册的组件。directives
: 全局注册的指令。provides
: 用于 provide/inject 的数据。optionsCache
: 组件选项缓存。propsCache
: props 选项缓存。emitsCache
: emits 选项缓存。renderCache
: 渲染函数缓存。emitted
: 记录已触发的事件。
这个
context
对象至关重要,它包含了应用运行时的所有全局状态。 所有的组件都共享这个上下文,从而可以访问全局配置、组件、指令等。 - 调用
-
创建 App 实例:
- 调用
createAppAPI(render, context)
函数创建一个 App 实例。 createAppAPI
接收渲染函数render
和应用上下文context
作为参数,并返回一个包含各种 API 的对象,例如component
(注册组件),directive
(注册指令),mount
(挂载应用) 等。render
函数是 Vue 3 的渲染器提供的,负责将虚拟 DOM 渲染成真实 DOM。
- 调用
-
重写
mount
方法:- 获取 App 实例上的
mount
方法。 - 重写
mount
方法,以便在挂载应用时,将根组件渲染到指定的容器中。 - 重写后的
mount
方法接收一个容器或选择器 (containerOrSelector
) 作为参数。 - 调用
normalizeContainer
函数将容器或选择器转换为 DOM 元素。 - 创建一个根组件的 VNode (虚拟节点)。
- 将应用上下文
context
赋值给根组件的 VNode 的appContext
属性。 这样,根组件及其所有子组件都可以访问应用上下文。 - 调用
render(vnode, container)
函数将根组件的 VNode 渲染到容器中。
- 获取 App 实例上的
-
返回 App 实例:
- 返回创建好的 App 实例。
三、createAppAPI
: App 实例的 API 工厂
createAppAPI
函数负责创建 App 实例,并为其添加各种 API,例如 component
, directive
, mount
等。 简化后的代码如下:
import {
createVNode,
render,
RendererNode,
RendererElement
} from './renderer'
import {
Component,
ComponentOptions
} from './component'
import {
AppContext
} from './apiCreateApp'
import {
isFunction,
isString
} from '@vue/shared'
export interface App<HostElement = any> {
component(name: string, component: Component): this
directive(name: string, directive: any): this
mount(rootContainer: HostElement | string): any
provide<T>(key: InjectionKey<T> | string | number, value: T): this
unmount(): void
config: any
version: string
}
type CreateAppFunction<HostElement> = (
rootComponent: Component,
rootProps?: Record<string, any>
) => App<HostElement>
export function createAppAPI<HostElement>(
render: (vnode: any, container: HostElement) => void,
context: AppContext
): CreateAppFunction<HostElement> {
return function createApp(...args) {
const app: App = {
version: '3.3.4', // 假设的版本号
config: context.config,
component(name: string, component: Component): any {
if (!component) {
return context.components[name]
}
context.components[name] = component
return app
},
directive(name: string, directive: any): any {
if (!directive) {
return context.directives[name]
}
context.directives[name] = directive
return app
},
mount(rootContainer: HostElement | string): any {
// 省略 mount 方法的实现,因为它已经在 createComponentApp 中被重写
},
provide(key: any, value: any): any {
if (__DEV__ && key in context.provides) {
warn(
`App already provides a value for key "${String(key)}". ` +
`It is recommended to use a unique key for each provide.`
)
}
context.provides[key as string | number] = value
return app
},
unmount(): void {
// TODO: implement unmount
}
}
return app
}
}
这个函数返回一个 createApp
函数,该函数接收根组件和根组件 props 作为参数,并返回一个 App 实例。 App 实例上包含了以下 API:
component(name, component)
: 注册或获取全局组件。directive(name, directive)
: 注册或获取全局指令。mount(rootContainer)
: 将应用挂载到指定的容器中。provide(key, value)
: 提供一个可以在组件树中注入的值。unmount()
: 卸载应用。config
: 应用配置对象。version
: Vue 的版本号。
四、mount
方法: 应用的启动器
mount
方法是 App 实例上最重要的 API 之一。 它负责将根组件渲染到指定的容器中,从而启动整个应用。
在 createComponentApp
函数中,我们重写了 App 实例上的 mount
方法。 让我们再次看一下重写后的 mount
方法的实现:
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return;
const vnode: VNode = createVNode(rootComponent, rootProps)
vnode.appContext = context
render(vnode, container)
}
这个 mount
方法做了以下几件事情:
-
获取容器:
- 调用
normalizeContainer
函数将容器或选择器转换为 DOM 元素。 - 如果容器不存在,则返回
null
。
- 调用
-
创建 VNode:
- 调用
createVNode(rootComponent, rootProps)
函数创建一个根组件的 VNode。 createVNode
函数是 Vue 3 的虚拟 DOM 创建函数,负责将组件选项转换为 VNode 对象。
- 调用
-
设置应用上下文:
- 将应用上下文
context
赋值给根组件的 VNode 的appContext
属性。 这样,根组件及其所有子组件都可以访问应用上下文。
- 将应用上下文
-
渲染 VNode:
- 调用
render(vnode, container)
函数将根组件的 VNode 渲染到容器中。 render
函数是 Vue 3 的渲染器提供的,负责将虚拟 DOM 渲染成真实 DOM。 它会遍历 VNode 树,创建对应的 DOM 元素,并将它们插入到容器中。
- 调用
五、总结: createApp
的核心流程
现在,让我们来总结一下 createApp
函数的核心流程:
步骤 | 函数 | 描述 |
---|---|---|
1 | createApp |
门面函数,调用 createComponentApp 创建应用实例。 |
2 | createComponentApp |
创建应用上下文 context ,并调用 createAppAPI 创建 App 实例。 |
3 | createAppContext |
创建应用上下文对象,包含应用配置、全局组件、指令等。 |
4 | createAppAPI |
创建 App 实例,并为其添加各种 API,例如 component , directive , mount 等。 |
5 | mount |
将根组件渲染到指定的容器中,启动整个应用。 |
六、render
函数: 渲染的引擎
render
函数是 Vue 3 渲染器的核心函数,它负责将虚拟 DOM 渲染成真实 DOM。 createApp
函数最终会调用 render
函数来将根组件渲染到容器中。
render
函数的实现比较复杂,涉及到虚拟 DOM 的 diff 算法、DOM 操作优化等。 这里我们不深入分析 render
函数的实现细节,只简单介绍一下它的主要流程:
-
Diff 算法:
render
函数首先会比较新旧 VNode 树的差异,找出需要更新的节点。- Vue 3 使用了一种优化的 diff 算法,可以高效地找出需要更新的节点。
-
DOM 操作:
- 对于需要更新的节点,
render
函数会执行相应的 DOM 操作,例如创建新的 DOM 元素、更新现有的 DOM 元素、删除不需要的 DOM 元素等。 - Vue 3 使用了一些优化技术,例如批量更新 DOM、避免不必要的 DOM 操作等,从而提高渲染性能。
- 对于需要更新的节点,
-
组件更新:
- 如果 VNode 对应的是一个组件,
render
函数会触发组件的更新流程。 - 组件的更新流程包括执行组件的生命周期钩子函数、重新渲染组件的模板等。
- 如果 VNode 对应的是一个组件,
七、实例演示
为了更好地理解 createApp
函数的用法,让我们来看一个简单的例子:
<!DOCTYPE html>
<html>
<head>
<title>Vue 3 App</title>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref } = Vue
const MyComponent = {
template: '<div>Hello, {{ message }}!</div>',
setup() {
const message = ref('Vue 3')
return { message }
}
}
const app = createApp(MyComponent)
app.mount('#app')
</script>
</body>
</html>
在这个例子中,我们首先引入了 Vue 3 的 CDN 链接。 然后,我们定义了一个名为 MyComponent
的组件,它包含一个 message
属性和一个简单的模板。
接下来,我们调用 createApp(MyComponent)
函数创建一个 App 实例,并将 MyComponent
作为根组件。 最后,我们调用 app.mount('#app')
函数将应用挂载到 id 为 app
的 DOM 元素中。
运行这个例子,你会在页面上看到 "Hello, Vue 3!" 的字样。
八、总结
恭喜你,已经成功完成了今天的 Vue 3 createApp
函数源码之旅! 我们深入剖析了 createApp
函数的内部逻辑,了解了它是如何初始化应用上下文并与渲染器连接的。
希望通过今天的讲解,你对 Vue 3 的启动流程有了更深入的理解。 记住,源码是最好的老师。 没事多看看 Vue 3 的源码,你会有意想不到的收获!
下次有机会,我们再聊聊 Vue 3 的其他有趣的特性。 拜拜!