各位靓仔靓女,大家好!我是今天的主讲人,咱们今天的主题是:Vue 3 源码剖析之 createApp
,带你一步步走进 Vue 3 的世界,看看应用实例是如何诞生的,渲染过程又是如何开始的。
准备好了吗?Let’s dive in!
一、createApp
:应用实例的起点
首先,让我们来看看 createApp
在 Vue 3 中扮演的角色。简单来说,createApp
是创建 Vue 应用实例的入口函数。它接收一个根组件作为参数,并返回一个应用实例对象,这个实例对象上挂载了一系列方法,用于控制应用的生命周期和行为。
createApp
的核心功能:
- 接收根组件: 这是应用的核心,决定了应用的初始 UI 结构。
- 创建应用实例: 生成一个包含各种属性和方法的应用对象。
- 提供配置能力: 允许你全局配置应用,比如注册组件、插件等。
- 启动渲染: 调用
mount
方法将应用挂载到 DOM 节点上,开始渲染。
二、源码探秘:createApp
做了什么?
接下来,我们深入 Vue 3 的源码,看看 createApp
内部到底做了哪些事情。由于源码比较庞大,我们只关注核心逻辑。
// packages/vue/src/createApp.ts
import { createComponentApp, defineCustomElement } from './apiCreateApp'
import { defineComponent } from './apiDefineComponent'
import { h } from './h'
import { provide } from './apiProvide'
import { nextTick } from './runtime'
import { version } from './version'
import { handleError } from './errorHandling'
export const createApp = ((...args) => {
const app = createComponentApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptions(app)
}
return app
}) as CreateAppFunction<Element>
export const createComponentApp = ((
rootComponent: any,
rootProps: any = null
) => {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to createApp() must be an object.`)
rootProps = null
}
const context = createAppContext()
const { mixins, components, directives } = rootComponent
const app = {
_uid: uid++,
_component: rootComponent,
_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[]) {
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)
}
} else if (__DEV__) {
warn(
'app.mixin() is deprecated in the Options API-less build of Vue. ' +
'Use global mixins instead.'
)
}
return app
},
component(name: string, component: Component) {
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in the app.`)
}
context.components[name] = component
return app
},
directive(name: string, directive: Directive) {
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in the app.`)
}
context.directives[name] = directive
return app
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
if (!compiled) {
// resolve global mixins
if (mixins && __FEATURE_OPTIONS_API__) {
mixins.forEach(mixin => app.mixin(mixin))
}
rootComponent = extend(context.mixins.reduce((ret, mixin) => extend(ret, mixin), {}), rootComponent)
}
// if (__DEV__) {
// devtoolsInitApp(app, version)
// }
const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root instance
// if (__DEV__) {
// devtoolsInitApp(app, version)
// }
if (isHydrate && hydrate) {
hydrate(vnode as any, rootContainer)
} else {
render(vnode, rootContainer, isSVG)
}
compiled = true
app._container = rootContainer
// for devtools and vue-router
;(rootContainer as any).__vue_app__ = app
return vnode.component.proxy
},
unmount() {
if (app._container) {
render(null, app._container)
// if (__DEV__) {
// devtoolsUnmountApp(app)
// }
}
},
provide(key: any, value: any) {
if (__DEV__ && key in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`
)
}
context.provides[key] = value
return app
}
}
return app
}) as CreateAppFunction<Element>
我们简化一下,提取核心代码段:
const app = {
_uid: uid++,
_component: rootComponent,
_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[]) { ... },
mixin(mixin: ComponentOptions) { ... },
component(name: string, component: Component) { ... },
directive(name: string, directive: Directive) { ... },
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 创建根组件的 VNode
const vnode = createVNode(rootComponent, rootProps);
// 将应用上下文存储到根 VNode 上
vnode.appContext = context;
// 调用 render 函数进行渲染
render(vnode, rootContainer, isSVG);
return vnode.component.proxy;
},
unmount() { ... },
provide(key: any, value: any) { ... }
}
让我们逐行解读:
-
app
对象: 这是createApp
返回的应用实例,包含了各种属性和方法。_uid
: 应用实例的唯一 ID。_component
: 根组件。_props
: 传递给根组件的 props。_container
: 应用挂载的 DOM 容器。_context
: 应用上下文,包含全局配置信息。_instance
: 根组件实例。version
: Vue 版本。config
: 应用的全局配置对象,可以通过它来修改应用的默认行为。use
: 用于注册插件。mixin
: 用于注册全局混入。component
: 用于注册全局组件。directive
: 用于注册全局指令。mount
: 用于将应用挂载到 DOM 节点上,启动渲染。unmount
: 用于卸载应用。provide
: 用于提供全局依赖。
-
mount
方法: 这是createApp
最关键的方法之一,负责将应用挂载到 DOM 节点上。- 创建 VNode: 首先,它会调用
createVNode
函数,将根组件转换为一个 VNode (Virtual DOM 节点)。VNode 是对真实 DOM 的一个轻量级描述,Vue 使用 VNode 来高效地更新 DOM。 - 存储应用上下文: 然后,它会将应用上下文
context
存储到根 VNode 上。这样,在组件树的任何地方,都可以通过 VNode 访问到应用的全局配置。 - 调用
render
函数: 最后,它会调用render
函数,将 VNode 渲染到指定的 DOM 容器中。render
函数会将 VNode 转换为真实 DOM 节点,并将其插入到容器中。
- 创建 VNode: 首先,它会调用
三、应用上下文:createAppContext
的秘密
在 createApp
的源码中,我们看到了 createAppContext
函数。这个函数负责创建一个应用上下文对象,它包含了应用的全局配置信息。
// packages/vue/src/createApp.ts
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: null,
isCustomElement: NO,
runtimeCompiler: __DEV__,
transition: null,
transitionGroup: null
},
provides: Object.create(null),
components: Object.create(null),
directives: Object.create(null),
mixins: [],
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap(),
renderCache: new WeakMap(),
directivesCache: new WeakMap(),
transitions: Object.create(null),
plugins: new Set(),
registered: Object.create(null),
filters: Object.create(null),
}
}
应用上下文包含了以下信息:
config
: 应用的全局配置对象,可以用来修改应用的默认行为。例如,可以设置全局的错误处理函数、警告处理函数等。provides
: 一个对象,用于存储全局依赖。可以使用app.provide
方法来注册全局依赖,然后在组件中使用inject
方法来注入这些依赖。components
: 一个对象,用于存储全局组件。可以使用app.component
方法来注册全局组件,然后在任何组件中直接使用这些组件。directives
: 一个对象,用于存储全局指令。可以使用app.directive
方法来注册全局指令,然后在模板中使用这些指令。mixins
: 一个数组,用于存储全局混入。可以使用app.mixin
方法来注册全局混入,这些混入会被应用到所有的组件中。
四、mount
的奥秘:启动渲染
mount
方法是启动 Vue 应用渲染的关键。它接收一个 DOM 元素作为参数,并将 Vue 应用挂载到该元素上。
让我们再次回顾 mount
方法的核心逻辑:
- 创建 VNode: 将根组件转换为 VNode。
- 存储应用上下文: 将应用上下文存储到根 VNode 上。
- 调用
render
函数: 将 VNode 渲染到 DOM 容器中。
render
函数的职责是将 VNode 转换为真实 DOM 节点,并将其插入到指定的 DOM 容器中。这个过程涉及到一系列复杂的算法,包括:
- Diff 算法: 比较新旧 VNode 之间的差异,找出需要更新的 DOM 节点。
- Patch 算法: 根据 Diff 算法的结果,更新 DOM 节点。
由于 render
函数的实现非常复杂,我们这里只简单介绍一下它的核心思想。
五、简化版 render
函数
为了方便理解,我们提供一个简化版的 render
函数:
function render(vnode, container) {
// 如果 VNode 是 null,则卸载应用
if (vnode === null) {
container.innerHTML = '';
return;
}
// 如果 VNode 是文本节点,则直接创建文本节点
if (typeof vnode === 'string') {
const textNode = document.createTextNode(vnode);
container.appendChild(textNode);
return;
}
// 如果 VNode 是组件,则创建组件实例并渲染
if (typeof vnode.type === 'object') {
const component = new vnode.type();
component.$el = render(component.render(), container);
return component.$el;
}
// 创建 DOM 元素
const el = document.createElement(vnode.type);
// 设置属性
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key]);
}
// 渲染子节点
if (vnode.children) {
if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => render(child, el));
} else {
render(vnode.children, el);
}
}
// 将 DOM 元素添加到容器中
container.appendChild(el);
return el;
}
这个简化版的 render
函数只实现了最基本的功能,但它可以帮助你理解 Vue 的渲染过程。
六、总结
让我们来回顾一下今天的内容:
createApp
是创建 Vue 应用实例的入口函数。createApp
接收一个根组件作为参数,并返回一个应用实例对象。- 应用实例对象包含了各种属性和方法,用于控制应用的生命周期和行为。
mount
方法用于将应用挂载到 DOM 节点上,启动渲染。render
函数负责将 VNode 转换为真实 DOM 节点,并将其插入到指定的 DOM 容器中。createAppContext
函数负责创建一个应用上下文对象,它包含了应用的全局配置信息。
下面用一个表格总结createApp
的核心流程
步骤 | 描述 | 涉及函数/对象 |
---|---|---|
1. 创建应用实例 | createApp 被调用,传入根组件。创建一个包含配置、组件、指令、上下文等信息的应用实例对象。 |
createApp , createAppContext |
2. 创建根组件VNode | mount 被调用,将根组件转换为一个 VNode。VNode 是对真实 DOM 的一个轻量级描述。 |
mount , createVNode |
3. 建立应用上下文 | 将应用上下文存储到根 VNode 上。这样,在组件树的任何地方,都可以通过 VNode 访问到应用的全局配置。 | mount |
4. 启动渲染 | 调用 render 函数,将 VNode 渲染到指定的 DOM 容器中。render 函数会将 VNode 转换为真实 DOM 节点,并将其插入到容器中。 |
mount , render |
通过今天的学习,你已经对 Vue 3 的 createApp
有了更深入的了解。希望这些知识能够帮助你在实际开发中更好地使用 Vue 3。
七、小彩蛋:实战演练
最后,我们来一个实战演练,看看如何使用 createApp
创建一个简单的 Vue 应用。
<!DOCTYPE html>
<html>
<head>
<title>Vue 3 App</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>{{ message }}</h1>
</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello Vue 3!'
}
}
}).mount('#app')
</script>
</body>
</html>
在这个例子中,我们首先引入 Vue 3 的 CDN 链接。然后,我们创建一个 Vue 应用实例,并将其挂载到 ID 为 app
的 DOM 元素上。
这个应用非常简单,它只是在页面上显示一条消息。但是,它展示了如何使用 createApp
创建一个基本的 Vue 应用。
好了,今天的讲座就到这里。希望大家有所收获!下次再见!