Vue 3 源码剖析:createApp
的魔法世界
大家好,欢迎来到今天的 Vue 3 源码探险之旅! 今天我们要聊聊 Vue 3 中一个非常重要的函数:createApp
。 别看它名字平平无奇,它可是 Vue 应用的“创世之神”,负责创建应用实例,并启动整个渲染流程。 准备好了吗? 让我们一起揭开它的神秘面纱!
1. 从 createApp
开始:你的 Vue 应用的起点
首先,让我们来看看 createApp
的庐山真面目。 在 Vue 3 中,createApp
函数位于 packages/vue/src/apiCreateApp.ts
文件中。 它的核心作用是创建一个应用实例,这个实例提供了一些方法,比如 mount
,用于将应用挂载到 DOM 元素上。
// packages/vue/src/apiCreateApp.ts
import {
createAppAPI,
CreateAppFunction
} from './apiCreateAppInner'
import {
warn
} from './warning'
// 暴露的 createApp 函数
export const createApp = ((...args) => {
const app = createAppAPI(render)(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
return app
}) as CreateAppFunction
简单来说,createApp
函数接收一个根组件作为参数,然后调用 createAppAPI(render)
创建一个应用实例,最后返回这个实例。其中 render
是渲染函数,是 Vue 3 的核心。
2. createAppAPI
:应用实例的工厂
createAppAPI
函数是一个高阶函数,它接收一个渲染函数 render
作为参数,并返回一个创建应用实例的函数。这个函数负责创建应用实例,并为应用实例添加一些常用的方法,比如 mount
、unmount
、component
、directive
、provide
等。
// packages/vue/src/apiCreateAppInner.ts
import {
createVNode,
render,
h,
VNode
} from './renderer'
import {
Component,
ComponentPublicInstance
} from './component'
import {
isString,
isFunction,
isObject,
extend
} from '@vue/shared'
export type CreateAppFunction<HostElement> = (
rootComponent: Component,
rootProps?: Record<string, any> | null
) => App<HostElement>
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>
): 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
}
let isMounted = false
const context = createAppContext()
const installedPlugins = new Set()
const app: 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, ...options) {
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) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
}
} else if (__DEV__) {
warn(
`App.mixin() is only available in builds that support options API.`
)
}
return app
},
component(name, component) {
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in the app.`)
}
context.components[name] = component
return app
},
directive(name, directive) {
if (!directive) {
return context.directives[name]
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in the app.`)
}
context.directives[name] = directive
return app
},
mount(
rootContainer: HostElement | string,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 创建根组件的 VNode
const vnode = createVNode(rootComponent as ConcreteComponent, rootProps)
// store app context on the root VNode.
vnode.appContext = context
// HMR root check
if (__DEV__) {
context.reload = () => {
render(null, rootContainer)
render(createVNode(rootComponent as ConcreteComponent, rootProps), rootContainer)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as any, rootContainer as any)
} else {
// 真正的挂载点! 调用 render 函数将 VNode 渲染到容器中
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and vue-router
;(rootContainer as any).__vue_app__ = app
return vnode.component?.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.n` +
`If you want to remount the same app, unmount it first by calling ` +
`app.unmount().`
)
}
},
unmount() {
if (isMounted) {
render(null, app._container)
if (__DEV__) {
;(app._container as any).__vue_app__ = null
}
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`
)
}
context.provides[key as string | symbol] = value
return app
}
}
return app
}
}
让我们来分解一下:
createAppAPI(render)
: 接收render
函数,这个render
函数是 Vue 3 的渲染器,负责将 VNode 渲染成真实的 DOM。-
createApp(rootComponent, rootProps = null)
: 实际创建应用实例的函数。- 参数:
rootComponent
: 根组件。rootProps
: 传递给根组件的 props。
- 内部逻辑:
- 创建一个
app
对象,这个对象就是 Vue 应用实例。 - 在
app
对象上添加一些方法,比如mount
、unmount
、component
、directive
、provide
等。 - 返回
app
对象。
- 创建一个
- 参数:
-
app.mount(rootContainer, isHydrate?, isSVG?)
: 将应用挂载到 DOM 元素上。- 参数:
rootContainer
: 要挂载到的 DOM 元素,可以是 DOM 元素本身,也可以是 CSS 选择器。isHydrate
: 是否进行服务端渲染 (SSR) 的 hydration。isSVG
: 是否是 SVG 元素。
- 内部逻辑:
- 调用
createVNode
创建根组件的 VNode。 - 调用
render
函数将 VNode 渲染到rootContainer
中。 - 设置
isMounted
标志为true
,表示应用已经挂载。
- 调用
- 参数:
3. 应用实例的构成:app
对象
现在,我们来仔细看看 app
对象,它包含了 Vue 应用的所有核心信息。
属性/方法 | 描述 |
---|---|
_uid |
应用的唯一 ID。 |
_component |
根组件。 |
_props |
传递给根组件的 props。 |
_container |
应用挂载的 DOM 容器。 |
_context |
应用上下文,包含配置信息、组件、指令等。 |
_instance |
根组件的组件实例。 |
version |
Vue 的版本。 |
config |
应用的配置信息,比如是否开启全局错误处理、是否开启性能追踪等。 |
use |
注册插件。 |
mixin |
注册全局 mixin。 |
component |
注册组件。 |
directive |
注册指令。 |
mount |
将应用挂载到 DOM 元素上,启动渲染流程。 |
unmount |
卸载应用,移除所有组件和指令。 |
provide |
在应用级别提供依赖注入,允许子组件访问父组件提供的数据。 |
4. mount
方法:渲染流程的启动器
app.mount
方法是整个渲染流程的启动器。 当你调用 app.mount('#app')
时,Vue 会做以下几件事情:
-
创建根组件的 VNode: 调用
createVNode(rootComponent, rootProps)
创建根组件的 VNode。 VNode 是 Virtual DOM 的节点,它是一个 JavaScript 对象,描述了组件应该渲染成什么样的 DOM 结构。// packages/vue/src/renderer.ts import { isString, isFunction, isObject } from '@vue/shared' import { createComponentVNode } from './vnode' export function createVNode( type: any, props: any = null, children: any = null ): VNode { const vnode = { type, props, children, el: null, // 对应的真实 DOM 元素 component: null, // 如果是组件 VNode,则指向组件实例 key: props?.key, } if (isString(type)) { // 元素节点 } else if (isObject(type) || isFunction(type)) { // 组件节点 return createComponentVNode(type, props, children) } return vnode } function createComponentVNode( Component: any, props: any, children: any ): VNode { const vnode = createVNode(Component, props, children) vnode.type = Component return vnode }
- 将应用上下文存储到 VNode 上:
vnode.appContext = context
。 这样,在渲染过程中,组件实例就可以访问到应用级别的配置信息和依赖注入。 -
调用
render
函数:render(vnode, rootContainer)
。render
函数是 Vue 3 的核心渲染器,它负责将 VNode 渲染成真实的 DOM 元素,并将其添加到rootContainer
中。// packages/vue/src/renderer.ts export function render(vnode, container) { patch(null, vnode, container) } function patch(n1, n2, container) { if (n1 === n2) { return } const { type } = n2 if (typeof type === 'string') { // 处理元素节点 processElement(n1, n2, container) } else if (typeof type === 'object') { // 处理组件节点 processComponent(n1, n2, container) } } function processElement(n1, n2, container) { if (!n1) { mountElement(n2, container) } else { // 更新元素节点 patchElement(n1, n2) } } function mountElement(vnode, container) { const { type, props, children } = vnode const el = (vnode.el = document.createElement(type)) // 创建 DOM 元素 if (props) { for (const key in props) { const value = props[key] el.setAttribute(key, value) // 设置属性 } } if (Array.isArray(children)) { // 处理子节点 mountChildren(children, el) } else if (typeof children === 'string') { el.textContent = children // 设置文本内容 } container.appendChild(el) // 将元素添加到容器中 } function mountChildren(children, container) { children.forEach(child => { patch(null, child, container) // 递归处理子节点 }) } function processComponent(n1, n2, container) { if (!n1) { mountComponent(n2, container) } else { // 更新组件节点 updateComponent(n1, n2) } } function mountComponent(initialVNode, container) { const instance = (initialVNode.component = createComponentInstance(initialVNode)) setupComponent(instance) setupRenderEffect(instance, initialVNode, container) } function createComponentInstance(vnode) { const instance = { vnode, type: vnode.type, props: {}, attrs: {}, slots: {}, ctx: {}, data: {}, setupState: {}, isMounted: false, } return instance } function setupComponent(instance) { // ... 省略设置 props, slots 等逻辑 instance.render = instance.type.render } function setupRenderEffect(instance, initialVNode, container) { const { render } = instance const componentUpdateFn = () => { if (!instance.isMounted) { // 首次渲染 const subTree = (instance.subTree = render.call(instance.proxy)) patch(null, subTree, container) initialVNode.el = subTree.el instance.isMounted = true } else { // 更新 const nextTree = render.call(instance.proxy) patch(instance.subTree, nextTree, container) instance.subTree = nextTree } } componentUpdateFn() }
简单来说,render
函数会递归遍历 VNode 树,将每个 VNode 节点转换成真实的 DOM 元素,并将其添加到 DOM 树中。 这个过程包括:
- 创建 DOM 元素: 根据 VNode 的
type
属性创建对应的 DOM 元素。 - 设置属性: 将 VNode 的
props
属性设置到 DOM 元素上。 - 处理子节点: 递归调用
render
函数处理 VNode 的子节点。 - 将 DOM 元素添加到容器中: 将创建的 DOM 元素添加到
rootContainer
中。
5. 总结:createApp
的角色
createApp
函数是 Vue 应用的入口,它负责:
- 创建应用实例,并为应用实例添加一些常用的方法。
- 创建根组件的 VNode。
- 调用
render
函数将 VNode 渲染成真实的 DOM 元素,并将其添加到 DOM 容器中。
createApp
的核心流程可以用以下表格概括:
步骤 | 描述 |
---|---|
1. 调用 createApp(rootComponent, rootProps) |
创建应用实例 app ,包含配置、插件、组件等。 |
2. 调用 app.mount(rootContainer) |
启动渲染流程。 |
3. createVNode(rootComponent, rootProps) |
创建根组件的 VNode (虚拟 DOM 节点)。 |
4. render(vnode, rootContainer) |
将 VNode 渲染到 rootContainer 中,这个过程会递归遍历 VNode 树,将每个 VNode 节点转换成真实的 DOM 元素,并将其添加到 DOM 树中。主要流程如下: 1. patch(null, vnode, rootContainer) : 对比新旧 VNode,决定是创建、更新还是删除 DOM 节点。 2. processElement(n1, n2, container) 或 processComponent(n1, n2, container) : 处理元素节点或组件节点。 3. 递归调用 patch 处理子节点。 |
6. 扩展思考:render
函数的奥秘
我们今天只是简单地介绍了 render
函数,但它实际上是一个非常复杂的函数,包含了 Vue 3 的核心渲染逻辑。 在 Vue 3 中,render
函数使用了 基于模板的编译优化 和 响应式系统 等技术,实现了高性能的渲染。
- 基于模板的编译优化: Vue 3 会将模板编译成一系列的渲染函数,这些渲染函数会直接操作 VNode,避免了不必要的 DOM 操作。
- 响应式系统: Vue 3 使用了基于 Proxy 的响应式系统,可以精确地追踪数据的变化,并在数据变化时只更新需要更新的 DOM 元素。
这些技术使得 Vue 3 在性能上有了很大的提升。
7. 总结:
今天我们深入探讨了 Vue 3 源码中 createApp
方法的实现原理。 我们了解了 createApp
函数的作用、应用实例的构成以及渲染流程的启动过程。 希望今天的分享能够帮助你更好地理解 Vue 3 的内部机制,并在实际开发中更加得心应手。
这次的源码探险就到这里,希望大家有所收获! 下次有机会再和大家一起探索 Vue 3 的其他奥秘。 谢谢大家!