本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 22 篇,关注专栏
前言
前面我们分别对 Vue
的 响应式
、render
、编译器
等内容的分析,至此我们还剩下最后一块内容,即 createAPP
函数的讲解。本篇也是 Vue3 源码解析系列
的最后一篇,下面我们依旧通过案例的形式来一探究竟。
案例一
首先引入 createApp
、h
两个函数,声明一个包含 render
方法的 APP
对象,通过 createApp
创建 app
对象,之后调用 mount
方法来挂载到对应的节点上。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { createApp, h } = Vue
const APP = {
render() {
return h('div', 'hello world')
}
}
const app = createApp(APP)
app.mount('#app')
</script>
</body>
</html>
CreateApp 函数
根据案例一我们得知,APP
对象类似于之前的 component
,而 createAPP
类似返回了一个 vnode
节点,并通过 mount
方法来进行 render
挂载。
理解完上述分析,我们再来看下 createApp
函数,它被定义在 packages/runtime-core/src/renderer.ts
文件中:
ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 省略
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
可见 createAPP
是由 createAppAPI
函数所返回的,我们再看下 createAppAPI
方法,它被定义在 packages/runtime-core/apiCreateApp.ts
文件中:
ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
// 省略
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
// 省略
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 省略
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 省略
if (isHydrate && hydrate) {
// 省略
} else {
render(vnode, rootContainer, isSVG)
}
// 省略
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
// 省略
}
},
// 省略
})
// 省略
return app
}
}
该方法会返回一个 createApp
函数,接收 组件
作为参数,然后创建一个 app
对象且返回,该对象包含了 mount
方法。这也就是为什么我们执行完 createApp
,可以直接调用 app.mount
。
我们再看下 mount
方法:
ts
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 省略
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 省略
if (isHydrate && hydrate) {
// 省略
} else {
render(vnode, rootContainer, isSVG)
}
// 省略
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
// 省略
}
},
该方法将 组件
通过 createVNode
生成 vnode
节点,然后执行 render
函数,将节点挂载到指定的容器上。但是我们发现我们传入的容器是一个字符串 '#app'
,可是 render
函数接收的 container
是一个元素,那 Vue
又是如何处理的呢?
我们知道 render
函数是通过 ensureRenderer().render(...args)
来执行的,那么 createAPP
也是一样,它被定义在 packages/runtime-dom/src/index.ts
文件中:
ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
// 省略
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
可以看到 container
是通过 normalizeContainer
来获取的:
ts
function normalizeContainer(
container: Element | ShadowRoot | string
): Element | null {
if (isString(container)) {
const res = document.querySelector(container)
if (__DEV__ && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
)
}
return res
}
// 省略
return container as any
}
该方法根据传入的 container
如果是字符串 (例如:'#app')
就重新获取元素且返回,这也就是为什么我们执行 app.mount('#app')
时可以直接渲染了。
另外还有种模板场景,Vue
是如何进行挂载的呢?我们再来看个例子。
案例二
首先引入 createApp
、h
两个函数,声明一个包含 template
模板的 APP
对象,通过 createApp
创建 app
对象,之后调用 mount
方法来挂载到对应的节点上。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { createApp, h } = Vue
const APP = {
template: '<div>hello world</div>'
}
const app = createApp(APP)
app.mount('#app')
</script>
</body>
</html>
模板渲染
要将模板进行渲染,首先要将 template
转换成 render
函数,这里就要调用 compiler
编译器。
根据案例执行 mount
方法触发 render
函数,接着执行 patch
方法触发 finishComponentSetup
方法:
ts
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
// 省略
// template / render function normalization
// could be already set when returned from setup()
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
// is done by server-renderer
if (!isSSR && compile && !Component.render) {
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template
if (template) {
// 省略
Component.render = compile(template, finalCompilerOptions)
// 省略
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
// 省略
}
// 省略
}
当前 vnode
节点的 type
类型为:
根据判断,当前组件不存在 render
函数,则执行 Component.render = compile(template, finalCompilerOptions)
,调用 compile
函数对模板进行编译返回 render
函数。此时 vnode
节点就具备了 render
函数,之后再将节点插入到指定容器中,执行完页面呈现:
至此,createApp
两种挂载场景都分析完毕。
总结
createApp
函数实际返回的是一个app
对象,里面包含了mount
方法。mount
方法接收的参数会通过normalizeContainer
函数进行处理,如果是字符串类型则会获取对应的元素。- 如果传入的是
template
模板类型,则会调用compile
进行编译生成render
函数。
Vue3 源码实现
Vue3 源码解析系列
- Vue3源码解析之 源码调试
- Vue3源码解析之 reactive
- Vue3源码解析之 ref
- Vue3源码解析之 computed
- Vue3源码解析之 watch
- Vue3源码解析之 runtime
- Vue3源码解析之 h
- Vue3源码解析之 render(一)
- Vue3源码解析之 render(二)
- Vue3源码解析之 render(三)
- Vue3源码解析之 render(四)
- Vue3源码解析之 render component(一)
- Vue3源码解析之 render component(二)
- Vue3源码解析之 render component(三)
- Vue3源码解析之 render component(四)
- Vue3源码解析之 render component(五)
- Vue3源码解析之 diff(一)
- Vue3源码解析之 diff(二)
- Vue3源码解析之 compiler(一)
- Vue3源码解析之 compiler(二)
- Vue3源码解析之 compiler(三)
- Vue3源码解析之 createApp