各位靓仔靓女们,晚上好!欢迎来到 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 的原理,在实际开发中更加得心应手。
下次再见! 别忘了点赞收藏哦!