各位靓仔靓女们,晚上好!欢迎来到 Vue 3 源码剖析小课堂。今天咱们的主题是:createApp 凭啥能创建应用实例,又是怎么开始渲染的?别慌,我会用最接地气的方式,带你们抽丝剥茧,扒光它的底裤(误)。
一、开场白:createApp 是个啥?
在 Vue 3 中,createApp 就像一个造物主,它负责创建一个 Vue 应用实例,这个实例就是咱们整个应用的核心。有了它,才能挂载组件、注册全局组件/指令/混入等等。
简单来说,没了 createApp,Vue 应用就是一堆散装零件,根本跑不起来。
二、源码初探:createApp 的真面目
咱们先看看 createApp 的源码,别怕,我会把关键部分拎出来:
// packages/vue/src/apiCreateApp.ts
import {
createAppAPI,
CreateAppFunction,
} from './apiCreateAppInner'
export const createApp = ((...args) => {
const app = createAppAPI(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
return app
}) as CreateAppFunction
别被 TypeScript 类型定义吓到,实际上 createApp 只是一个包装函数,它调用了 createAppAPI 函数,并返回一个应用实例。__DEV__ 是一个编译时常量,表示是否是开发环境,如果是开发环境,会做一些额外的检查。
所以,真正的核心逻辑都在 createAppAPI 函数里。
三、createAppAPI:应用实例的制造工厂
// packages/vue/src/apiCreateAppInner.ts
import {
Component,
ComponentPublicInstance,
createVNode,
VNode,
} from './vnode'
import {
render,
RootRenderFunction,
createRenderer,
RendererOptions,
} from './renderer'
import {
isString,
isFunction,
extend,
isObject,
} from '@vue/shared'
import {
ComponentOptions,
ConcreteComponent,
validateComponentName,
} from './component'
import {
AppContext,
createAppContext,
} from './apiCreateAppContext'
import {
warn,
DeprecationTypes,
deprecationContext,
} from './warning'
import {
Directive,
validateDirectiveName,
} from './directives'
import {
isRuntimeOnly,
isCompatEnabled,
compatUtils,
} from './compat/compatConfig'
export interface CreateAppFunction<HostElement> {
<T>(rootComponent: Component, rootProps?: Record<string, any>): App<T>
}
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootRenderFunction<HostElement>
): CreateAppFunction<HostElement> {
return function createApp(...args) {
if (__DEV__ && !__TEST__) {
deprecationContext.set(DeprecationTypes.GLOBAL_MOUNT)
}
const [rootComponent, rootProps] = args
// 省略类型检查部分
const context = createAppContext() // 创建应用上下文
const installedPlugins = new Set()
let isMounted = false
const app: App = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps || null,
_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 (__DEV__ && !isObject(mixin)) {
warn(
`Mixin argument must be an object. Received ${String(mixin)}`
)
}
context.mixins.push(mixin)
return app
},
component(name: string, component?: Component): any {
if (!component) {
return context.components[name]
}
if (__DEV__ && !isString(name)) {
warn(`Component name must be a string. Received ${String(name)}`)
}
if (__DEV__ && !validateComponentName(name, context.components)) {
return app
}
context.components[name] = component
return app
},
directive(name: string, directive?: Directive): any {
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && !isString(name)) {
warn(`Directive name must be a string. Received ${String(name)}.`)
}
if (__DEV__ && !validateDirectiveName(name)) {
return app
}
context.directives[name] = directive
return app
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 创建 VNode
const vnode = createVNode(rootComponent as ConcreteComponent, rootProps)
// 将应用实例注入到 VNode 的 appContext 中,供子组件使用
vnode.appContext = context
// 如果是服务端渲染,则进行激活
if (isHydrate && hydrate) {
hydrate(vnode as any, rootContainer)
} else {
// 调用 renderer.render 方法进行渲染
render(vnode as any, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and vue-router
;(rootContainer as any).__vue_app__ = app
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted. Create a new app instance instead.`
)
}
},
unmount() {
if (isMounted) {
render(null, app._container)
if (__DEV__) {
;(app._container as any).__vue_app__ = null
}
app._component = null
app._container = null
app._instance = null
isMounted = false
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
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
}
}
if (__DEV__) {
injectRegisterHook(app)
}
return app
}
}
这段代码略长,但别担心,我来给大家划重点:
-
createAppContext(): 创建一个应用上下文对象context,这个对象存储了应用级别的配置、组件、指令、混入等信息。相当于应用的全局状态仓库。 -
app对象:createAppAPI返回一个app对象,这个对象就是咱们的应用实例。它包含了以下关键属性和方法:_component:根组件。_props:传递给根组件的 props。_container:挂载的 DOM 元素。_context:应用上下文。use():注册插件。mixin():注册全局混入。component():注册全局组件。directive():注册全局指令。mount(): 将应用挂载到 DOM 元素上,这是启动渲染的关键方法。unmount():卸载应用。provide():提供可以在应用中的所有组件中访问的依赖项。
-
mount()方法:mount方法是整个渲染过程的入口,它做了以下几件事:createVNode(): 创建一个 VNode (虚拟节点),VNode 是对真实 DOM 的抽象,Vue 3 使用 VNode 来进行高效的更新。vnode.appContext = context: 将应用上下文context注入到 VNode 中,这样子组件就可以访问到全局配置等信息了。render(): 调用renderer.render()方法,将 VNode 渲染到指定的 DOM 容器中。这就是真正的渲染过程。
四、createRenderer:渲染器的制造工厂
咱们接着往下挖,看看 render 函数是怎么来的。在 createAppAPI 函数中,我们可以看到:
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootRenderFunction<HostElement>
): CreateAppFunction<HostElement> {
// ...
}
createAppAPI 接收一个 render 函数作为参数。这个 render 函数实际上是由 createRenderer 函数创建的。
// packages/vue/src/renderer.ts
import {
// ...省略一堆导入
} from '@vue/shared'
export interface RendererOptions<
HostNode = any,
HostElement = any
> {
patchProp: (
el: HostElement,
key: string,
prevValue: any,
nextValue: any,
isSVG?: boolean,
prevChildren?: VNode[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
unmountChildren?: UnmountChildrenFn
) => void
insert: (el: HostNode, parent: HostElement, anchor?: HostNode | null) => void
remove: (el: HostNode) => void
createElement: (
type: string,
isSVG?: boolean,
vnode?: VNode,
props?: any
) => HostElement
createText: (text: string) => HostNode
createComment: (text: string) => HostNode
setText: (node: HostNode, text: string) => void
setElementText: (el: HostElement, text: string) => void
parentNode: (node: HostNode) => HostElement | null
nextSibling: (node: HostNode) => HostNode | null
querySelector?: (selector: string) => HostElement | undefined
setScopeId?: (el: HostElement, id: string) => void
cloneNode?: (node: HostNode) => HostNode
insertStaticContent?: (
content: string,
container: HostElement,
anchor: HostNode | null,
isSVG: boolean
) => [HostNode, HostNode]
}
export function createRenderer<HostNode, HostElement>(
options: RendererOptions<HostNode, HostElement>
) {
// ... 省略一堆内部函数定义,比如 patch、mountChildren 等
const render: RootRenderFunction<HostElement> = (
vnode: VNode | null,
container: HostElement,
isSVG?: boolean
) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(null, vnode, container, null, null, null, isSVG)
}
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
createRenderer 接收一个 options 对象作为参数,这个 options 对象包含了各种 DOM 操作方法,比如 createElement、patchProp、insert 等等。Vue 3 会根据这些方法,创建一个针对特定平台的渲染器。
比如,在浏览器环境中,options 对象会包含浏览器提供的 DOM API;在 NativeScript 环境中,options 对象会包含 NativeScript 提供的 API。
createRenderer 返回一个对象,包含了 render 函数、hydrate 函数 和 createApp 函数。这里的 createApp 函数,就是我们最开始看到的 createAppAPI 函数,它被注入了当前渲染器的 render 和 hydrate 方法。
重点来了! render 函数内部调用了 patch 函数,patch 函数才是真正负责将 VNode 渲染成真实 DOM 的核心函数。
五、patch 函数:新旧 VNode 的终极对决
patch 函数是 Vue 3 渲染器的核心,它负责比较新旧 VNode,并根据差异更新 DOM。由于 patch 函数的代码非常复杂,咱们这里只简单介绍一下它的工作原理:
-
判断 VNode 类型:
patch函数首先会判断 VNode 的类型,比如是组件、元素、文本等等。 -
处理不同类型的 VNode: 针对不同类型的 VNode,
patch函数会采取不同的处理方式。- 组件:
patch函数会创建或更新组件实例,并递归地patch组件的根 VNode。 - 元素:
patch函数会创建或更新 DOM 元素,并patch元素的子节点。 - 文本:
patch函数会创建或更新文本节点。
- 组件:
-
Diff 算法:
patch函数使用 Diff 算法来比较新旧 VNode 的子节点,找出需要更新的 DOM 节点。Vue 3 使用了更高效的 Diff 算法,可以更快地更新 DOM。 -
更新 DOM: 根据 Diff 算法的结果,
patch函数会更新 DOM,比如添加、删除、移动、修改 DOM 节点。
六、流程总结:createApp 到渲染的完整路径
为了更好地理解整个过程,咱们用一张表格来总结一下:
| 步骤 | 涉及函数 | 描述 |
|---|---|---|
| 1. 创建应用实例 | createApp -> createAppAPI |
createApp 函数调用 createAppAPI 函数,createAppAPI 函数创建一个应用实例 app,并初始化应用上下文 context。 |
| 2. 获取渲染器 | createRenderer |
createRenderer 函数接收一个 options 对象,包含了各种 DOM 操作方法,并返回一个渲染器对象,包含了 render 函数。 |
| 3. 挂载应用 | app.mount |
app.mount 方法接收一个 DOM 元素作为参数,创建一个根组件的 VNode,并将应用上下文 context 注入到 VNode 中。 |
| 4. 渲染 VNode | render -> patch |
render 函数接收一个 VNode 和一个 DOM 元素作为参数,调用 patch 函数将 VNode 渲染到 DOM 元素中。patch 函数比较新旧 VNode,并根据差异更新 DOM。 |
七、举个栗子:从代码到页面
// App.vue
<template>
<h1>{{ message }}</h1>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const message = ref('Hello, Vue 3!')
return {
message
}
}
}
</script>
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
这段代码的执行流程如下:
createApp(App):创建一个应用实例app,根组件是App.vue。app.mount('#app'):将应用挂载到 ID 为app的 DOM 元素上。mount函数内部:createVNode(App):创建一个App.vue组件的 VNode。render(vnode, document.querySelector('#app')):调用渲染器的render函数,将App.vue组件的 VNode 渲染到#app元素中。
render函数内部:patch(null, vnode, document.querySelector('#app')):调用patch函数,由于是首次渲染,所以第一个参数是null。patch函数会创建<h1>元素,并将message的值 "Hello, Vue 3!" 渲染到<h1>元素中。- 最终,
<h1>Hello, Vue 3!</h1>会出现在页面上。
八、总结与思考
今天咱们一起扒了扒 Vue 3 createApp 的底裤,从 createApp 到 createRenderer,再到 patch 函数,整个渲染流程就清晰多了。
当然,Vue 3 的源码远不止这些,还有很多细节值得我们深入研究。比如,组件的生命周期、响应式系统、编译器等等。
希望今天的分享能帮助大家更好地理解 Vue 3 的原理,在实际开发中更加得心应手。
下次再见! 别忘了点赞收藏哦!