Vue3源码学习笔记—— Vue.createApp(App) 和 app.mount(“#app“)(三)
本篇文章对 app.mount("#app") 实现流程进行了详细分析,以该系列(一)中的 demo 为例。
前情回顾
本篇文章对 app.mount(“#app”) 实现流程进行了详细分析,以该系列文章(一)中的 demo 为例。
戳链接回顾该系列文章(一)
Vue3源码学习笔记—— Vue.createApp(App) 和 app.mount(“#app”)(一) - 掘金 (juejin.cn)
戳链接回顾该系列文章(二)—— Vue.createApp(App) 做了什么
Vue3源码学习笔记—— Vue.createApp(App) 和 app.mount(“#app”)(二) - 掘金 (juejin.cn)
mount 实现流程
重写 mount 函数
位置:packages/runtime-dom/src/index.ts
createApp 函数中,首先取出 app 对象中的 mount 函数,然后通过 app.mount = () => {}
对 mount 函数进行重写:
① 首先调用 normalizeContainer 函数来获取container节点;
② 判断该节点是否存在,若不存在,则直接返回;
③ 清空container的innerHTML;
④ 调用mount函数。
export const createApp = ((...args) => {
// 1.创建app对象
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
// 2.这里取出了app中的mount方法,因为下面要进行重写
const { mount } = app
// 3.重写mount方法
/*
(1)这里重写的目的是考虑到跨平台(app.mount里面只包含和平台无关的代码)
(2)这些重写的代码都是一些和web关系比较大的代码(比如其他平台也可以进行类似的重写)
*/
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
component.template = container.innerHTML
// 2.x compat check
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null
)
break
}
}
}
}
// 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>
mount 函数
位置:/packages/runtime-core/src/apiCreateApp.ts
核心流程:① 根据传入的根组件App创建vnode;② 渲染vnode。
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
// 1.创建根组件的vnode
// 使用createVNode来创建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 = () => {
(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 2.渲染vnode
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
}
render 函数
位置:/packages/runtime-core/src/renderer.ts
在 mount 函数中,通过调用 render 函数来实现渲染vnode,而 render 函数是 baseCreateRenderer 函数返回调用 createAppAPI 时传入的参数之一,也就是说,render 函数是在 baseCreateRenderer 函数中定义的。
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
......
const render: RootRenderFunction = (vnode, container, isSVG) => {
// 如果vnode为null,那么就会销毁组件
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建或者更新组件都是使用patch函数(这里就是将根组件挂载到DOM上)
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
// 放到container上面,缓存vnode
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
该函数中,由于我们在 mount 中创建了 vnode,vnode 存在,因此调用 patch 函数,而此时 container._vnode
是不存在的,所以相当于 patch(null, vnode, container)
。
patch 函数
位置:/packages/runtime-core/src/renderer.ts
patch 对传入的 vnode 进行类型的判断,由于我们一开始传入的参数为根组件 App,因此,接下来调用 processComponent 函数来处理组件。
const patch: PatchFn = (
n1, // n1 表示旧的vnode,当n1为null时就表示是一次挂载(挂载or更新由n1决定)
n2, // n2 表示新的vnode,根据n2的type进行不同的处理
container, // 渲染后会将vnode渲染到container上
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
// 如果新的节点和旧的节点类型不同,那么会销毁整个子节点树
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text: ... //处理文本节点
case Comment: ... //处理注释节点
case Static: ... //处理静态节点
case Fragment: ... //处理Fragment组件节点
default:
if (shapeFlag & ShapeFlags.ELEMENT) { ... } // 处理普通的DOM元素,比如div/button/span
else if (shapeFlag & ShapeFlags.COMPONENT) { // 处理组件节点
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
else if (shapeFlag & ShapeFlags.TELEPORT) { ... }
else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ... }
else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) { ... }
}
processComponent 函数
位置:/packages/runtime-core/src/renderer.ts
processComponent 函数对传入的 n1(旧的 vnode )进行判断,若 n1 为空,则挂载节点,若 n1 不为空,则更新组件。此处 n1 为空,因此接下来执行 mountComponent 函数挂载组件。
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
// n1等于null,表示挂载节点
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
...
} else {
// 调用mountComponent挂载组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else { // n1不为null,表示更新组件
updateComponent(n1, n2, optimized)
}
}
mountComponent 函数
位置:/packages/runtime-core/src/renderer.ts
mountComponent 函数首先调用 ComponentInternalInstance()
创建组件的实例对象,该实例对象有很多属性,但都置空,而后通过调用 setupComponent(instance)
函数来对组件所有的数据进行操作和赋值。处理完组件实例对象的数据后,调用设置和渲染有副作用的函数 setupRenderEffect()
const mountComponent: MountComponentFn = (
...
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
// 1.调用ComponentInternalInstance创建组件的实例
const instance: ComponentInternalInstance =
compatMountInstance ||
// 调用createComponentInstance函数创建一个实例对象,其属性皆为没有值
(initialVNode.component = createComponentInstance(
...
))
......
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
// 2.setup组件实例,作用是对组件的props/slots/data等进行初始化处理
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
......
// 调用设置和渲染有副作用的函数
setupRenderEffect(
...
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
SetupRenderEffectFn 函数
位置:/packages/runtime-core/src/renderer.ts
SetupRenderEffectFn 函数首先判断传入组件是否已被挂载,若没有被挂载,则挂载组件,并在挂载完把组件的 isMounted 属性设置为true,表示已挂载;若已被挂载,则更新组件。
此时我们传入的组件是 根组件 App,应该挂载组件,递归调用 patch 函数挂载组件。
const setupRenderEffect: SetupRenderEffectFn = (
...
) => {
const componentUpdateFn = () => {
// 如果组件没有被挂载,那么挂载组件
if (!instance.isMounted) {
......
if (el && hydrateNode) {
......
} else {
......
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
initialVNode.el = subTree.el
}
......
instance.isMounted = true
......
} else { // 更新组件
......
}
}
.....
}
在 vue2 中,一个 template 只能有一个根元素,即 template 中的结构为:
<template>
<div>
<h2>Oooorange</h2>
<p>{{message}}</p>
<button @click="changeMessage">修改message</button>
</div>
</template>
而 vue3 中,一个 template 内是可以有多个根元素的,那是因为,当根元素有多个时,vue3会将多个根元素用 Fragment
元素包裹起来,则实际上 template 中的结构为:
<template>
<Fragment>
<h2>Oooorange</h2>
<p>{{message}}</p>
<button @click="changeMessage">修改message</button>
</Fragment>
</template>
来到 patch 函数,需要通过判断传入的组件类型执行相应的操作,而 demo 中 template 内有多个根元素,因此此时传入的组件类型应为 Fragment ,因此接下来执行 processFragment 函数。
const patch: PatchFn = (
...
) => {
...
switch (type) {
...
case Fragment: //处理Fragment组件节点
processFragment( ... )
break
default: ...
}
...
}
processFragment 函数
位置:/packages/runtime-core/src/renderer.ts
与上述 processComponent 函数执行思路相似,patch 函数判断类型后执行的函数都是这个思路,对传入的旧vnode进行判断并作出对应操作,即对传入的 n1(旧的 vnode )进行判断,若 n1 为空,则挂载节点,若 n1 不为空,则更新组件 。
processFragment 函数中,此处 n1 为空,因此接下来执行 mountChildren 函数。
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
...
) => {
...
if (n1 == null) { // n1等于null,表示挂载节点
...
mountChildren( ... )
} else { // n1不为null,表示更新组件
patchChildren( ... )
}
}
mountChildren 函数
位置:/packages/runtime-core/src/renderer.ts
mountChildren 函数遍历 children ,采用了深度遍历的思想挂载所有子元素。
const mountChildren: MountChildrenFn = (
children,
container,
。。。
) => {
// 遍历children,调用对应的patch方法
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
// 调用patch,如果继续有子节点就会一次执行(其实是一个深度优先的算法)
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
到这里,我们就已经进入普通的 DOM 元素的遍历了,进入 patch 函数后,将执行 processElement 函数处理普通的 DOM 元素。
const patch: PatchFn = (
...
) => {
...
switch (type) {
...
default:
if (shapeFlag & ShapeFlags.ELEMENT) { // 处理普通的DOM元素,比如div/button/span
processElement( ... )
} ...
}
...
}
processElement 函数
位置:/packages/runtime-core/src/renderer.ts
processElement 函数中,此处 n1 为空,因此接下来执行 mountElement 函数。
const processElement = ( ...
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
// 判断n1是否为null,为null表示要挂载节点
if (n1 == null) { // 调用mountElement挂载节点
mountElement( ... )
} else { // 不为null表示要更新节点
patchElement( ... )
}
}
mountElement 函数
位置:/packages/runtime-core/src/renderer.ts
mountElement 函数核心流程:
① 根据类型和其他属性,创建DOM元素节点;
② 判断子节点的类型,若子节点为纯文本,则直接处理纯文本,若子节点为数组,则调用 mountChildren 函数深度遍历处理子节点;
③ 处理 props 属性;
④ 将el挂载到 container 中。
const mountElement = (
vnode: VNode,
container: RendererElement,
...
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
if ( ... ) { ...
} else {
// 1.根据类型和其他属性,创建DOM元素节点
el = vnode.el = hostCreateElement( // 相当于调用document.createElement()
vnode.type as string,
isSVG,
props && props.is,
props
)
// 2.如果子节点是纯文本的情况,则调用 hostSetElementText 函数处理纯文本
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 3.如果子节点是一个数组的情况
mountChildren( ... )
}
...
// props
// 4.处理props属性<div class="" style="">
if (props) { ... }
// scopeId
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
}
...
// 5.调用该方法将el挂载到container中
hostInsert(el, container, anchor)
...
}
总结
mount 实现流程总结
到这里,我们就执行完 demo 中组件的挂载了,下面我用一张流程图对整体流程进行总结。
Vue.createApp(App) 和 app.mount(“#app”) 总结
最后
Vue.createApp(App) 和 app.mount(“#app”) 的内部实现原理到此就告一段落啦,如有错误,欢迎指正;尚有不足,请多指教~~~
更多推荐
所有评论(0)