最近有些时间,就想着深入vue3源码的实现背后,看下vue3应用初始化挂载的过程都有哪些步骤,每一个步骤大概做了哪些具体内容。
说明
:文章源码部分代码仅仅展示主要功能实现部分,如需查看完整源码,请移步GitHub vue-next
首先,我们以下面的代码来看下我们是如何创建和挂载Vue3应用的
<!DOCTYPE html>
<html lang="en">
<head>
<title>hello vue3</title>
<script src="../packages/vue/dist/vue.global.js"></script>
</head>
<body>
<div id='app'>app</div>
<script type="module">
const { createApp, h, ref } = Vue;
const App = {
setup() {
const num = ref(0);
const btnClick = () => {
num.value += 1;
}
return () => h(
'button',
{
onclick: btnClick
},
`点我+1--${num.value}`)
}
}
const app = createApp(App).mount("#app");
console.log(app);
</script>
</body>
</html>
在上面的代码中,我们可以看到,我们首先通过createApp()
方法来创建应用,然后调用createApp()
方法返回的实例对象属性方法mount()
挂载到指定Dom容器。
1. 入口文件
vue3源码的入口文件位于:packages\vue\src\index.ts
主要功能代码:
if (__DEV__) {
// dev环境提示,生产环境引入生产包
initDev()
}
// 缓存编译结果
const compileCache: Record<string, RenderFunction> = Object.create(null)
/**
* @description 将template转换为render函数
* $mount内部,在没有render函数的情况下,会将template转换render
*/
function compileToFunction(
template: string | HTMLElement, // 模板
options?: CompilerOptions // 编译配置
): RenderFunction {
// 如果 template 不是字符串
if (!isString(template)) {
if (template.nodeType) {
// 则认为是一个 DOM 节点,获取 innerHTML
template = template.innerHTML
} else {
__DEV__ && warn(`invalid template option: `, template)
return NOOP
}
}
const key = template
const cached = compileCache[key]
// 有缓存直接返回
if (cached) {
return cached
}
// template可以为id选择器,或者普通字符串
// 如果是 ID 选择器,则获取 DOM 元素,取 innerHTML作为template内容生成render
// 如果为普通字符串,则将其作为内容生成render
if (template[0] === '#') {
const el = document.querySelector(template)
if (__DEV__ && !el) {
warn(`Template element not found or is empty: ${template}`)
}
template = el ? el.innerHTML : ``
}
// 调用 compile 获取可执行代码code
const { code } = compile(
template,
extend(options)
)
// 将 code 转化为 function
const render = (__GLOBAL__
? new Function(code)()
: new Function('Vue', code)(runtimeDom)) as RenderFunction
// 标记template to render 完成
(render as InternalRenderFunction)._rc = true
// 返回 render 方法的同时,将其放入缓存
return (compileCache[key] = render)
}
// 通过依赖注入的方式,将 compile 函数注入至 runtime 运行时中
registerRuntimeCompiler(compileToFunction)
文件功能点说明:
- 声明变量用以缓存编译结果
- 调用registerRuntimeCompiler方法,将模板编译函数注入运行时。
1.1 registerRuntimeCompiler()方法:
let compile: CompileFunction | undefined
export function registerRuntimeCompiler(_compile: any) {
compile = _compile
}
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
if (!instance.render) {
const template = Component.template;
Component.render = compile(template, finalCompilerOptions)
}
}
其主要功能就是将compileToFunction方法添加为render方法。
2. createApp()方法
在入口文件packages\vue\src\index.ts的最后有一行有导出
// packages\vue\src\index.ts
export * from '@vue/runtime-dom'
接着看packages\runtime-dom\src\index.ts文件
// packages\runtime-dom\src\index.ts
export const createApp = ((...args) => {
// 获取应用实例对象
const app = ensureRenderer().createApp(...args)
// 保存app实例mount方法
const { mount } = app
// 重写实例的mount方法;我们在createApp().mount()就是此方法。
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 获取挂载点根元素容器
const container = normalizeContainer(containerOrSelector)
if (!container) return
//
const component = app._component
// 渲染优先级:render > template > container.innerHTML
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 清空挂载容器内容
container.innerHTML = '';
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
// 挂载容器添加app属性标识
container.setAttribute('data-v-app', '')
}
// 返回根组件实例
return proxy
}
// 返回应用实例,支持链式调用
return app
}) as CreateAppFunction<Element>
文件功能点说明:
- 定义createApp()方法
- 缓存应用实例mount方法并重写mount
- mount流程
- 获取挂载元素
- 渲染优先级:render > template > container.innerHTML
- 清空挂载容器
- mount挂载应用并返回应用实例
2.1 ensureRenderer()
方法可以理解为创建渲染器
// packages\runtime-dom\src\index.ts
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
// render渲染器存在直接返回
// 不存在则调用createRenderer方法创建渲染器render并缓存
// createRenderer方法传入参数rendererOptions(平台相关dom操作方法的封装)
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
2.2 createRenderer()
方法功能可以理解为创建渲染器方法
// packages\runtime-core\src\renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
方法比较简单,通过调用并返回baseCreateRenderer方法实现
2.3 baseCreateRenderer
创建渲染器
// packages\runtime-core\src\renderer.ts
// 此方法定义了一系列patch相关的处理方法,我们只看主要的结构
function baseCreateRenderer(
options: RendererOptions, // 平台相关 dom节点和属性操作方法
createHydrationFns?: typeof createHydrationFunctions
): any {
// 获取传入options 平台相关节点操作方法
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
forcePatchProp: hostForcePatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// 定义核心patch方法
const patch: PatchFn = () => {}
...
// 作为createAppAPI参数传入,app.mount()阶段在createVNode()之后调用
const render: RootRenderFunction = (vnode, container, isSVG) => {
// vnode 新传入虚拟dom
// container._vnode 旧虚拟dom
if (vnode == null) {
if (container._vnode) {
// 新vnode为空,存在旧vnode情况下,销毁旧组件
unmount(container._vnode, null, null, true)
}
} else {
// 创建or更新
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
// 保存vnode
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
2.4 createAppAPI
// runtime-core\src\apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
// rootComponent根组件应用配置,也就是createApp(args)传入的args参数
return function createApp(rootComponent, rootProps = null) {
// appContext对象
const context = createAppContext()
// 保存当前注册plugin
const installedPlugins = new Set()
// 当前未挂载标识
let isMounted = false
const app: App = (context.app = {
// 注册实例方法,app.use(plugin)
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
// plugin 注册不能重复
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
// plugin是一个对象,对象包含有install:function
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
// plugin 是一个function
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
},
// 注册实例方法app.mixin
mixin(mixin: ComponentOptions) {
// mixin只能用于options api
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
if (mixin.props || mixin.emits) {
context.deopt = true
}
} else if (__DEV__) {
warn(
'Mixin has already been applied to target app' +
(mixin.name ? `: ${mixin.name}` : '')
)
}
} else if (__DEV__) {
warn('Mixins are only available in builds supporting Options API')
}
return app
},
// 注册实例方法app.component
component(name: string, component?: Component): any {
if (__DEV__) {
// 验证组件name,不能用html标签作为组件名称
validateComponentName(name, context.config)
}
// component 不能重复注册
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
context.components[name] = component
return app
},
// 注册实例方法 app.directive
directive(name: string, directive?: Directive) {
if (__DEV__) {
// 校验指令名称合法性
validateDirectiveName(name)
}
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},
// 注册app.mount()方法,在packages\runtime-dom\src\index.ts文件createApp方法内部,重写既是该方法
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 获取vnode
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// vnode渲染真实dom
render(vnode, rootContainer, isSVG)
}
// 挂载标志位
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
// 返回挂载根组件实例
return vnode.component!.proxy
}
},
unmount() {
if (isMounted) {
// 卸载
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsUnmountApp(app)
}
delete app._container.__vue_app__
} 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] = value
return app
}
})
// 返回app实例
return app
}
}
2.5 createApp()方法内部流程总结
- 方法导出位置
// packages\runtime-dom\src\index.ts
/**
* @param {args} 根组件实例
*/
export const createApp = ((...args) => {
// 获取应用实例对象
const app = ensureRenderer().createApp(...args)
...
...
// 返回应用实例,支持链式调用
return app
})
- ensureRender
// packages\runtime-dom\src\index.ts
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
- createRenderer
// packages\runtime-core\src\renderer.ts
/**
* @param {options} 平台相关dom方法
*/
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
- baseCreateRenderer
// packages\runtime-core\src\renderer.ts
/**
* @param {options} 平台相关dom方法
*/
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
...
...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
- createAppAPI
// runtime-core\src\apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
// rootComponent根组件应用配置,也就是createApp(args)传入的args参数
return function createApp(rootComponent, rootProps = null) {
// appContext对象
const context = createAppContext()
// 保存当前注册plugin
const installedPlugins = new Set()
// 当前未挂载标识
let isMounted = false
const app: App = (context.app = {
// 注册实例方法,app.use(plugin)
use(plugin: Plugin, ...options: any[]) {
....
// 返回应用实例,支持链式调用
return app
},
// 注册实例方法app.mixin
mixin(mixin: ComponentOptions) {
....
return app
},
// 注册实例方法app.component
component(name: string, component?: Component): any {
return app
},
// 注册实例方法 app.directive
directive(name: string, directive?: Directive) {
return app
},
// 注册app.mount()方法
mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any {
// 返回挂载根组件实例
return vnode.component!.proxy
},
unmount() {},
provide(key, value) {
return app
}
})
// 返回app实例
return app
}
}
createApp方法内部主要有以下实现:
- 初始化创建appContext对象
- 变量声明缓存已注册的插件
- 定义app实例对象并声明初始化app应用Api方法:use(),mixin(),component(),directive(),mount(),unmount(),provide()