赞
踩
本篇文章对 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)
位置: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>
位置:/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)\`` ) } }
位置:/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)
。
位置:/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) { ... } }
位置:/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) } }
位置:/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`) } }
位置:/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: ...
}
...
}
位置:/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( ... )
}
}
位置:/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( ... )
} ...
}
...
}
位置:/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( ... )
}
}
位置:/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) ... }
到这里,我们就执行完 demo 中组件的挂载了,下面我用一张流程图对整体流程进行总结。
Vue.createApp(App) 和 app.mount(“#app”) 的内部实现原理到此就告一段落啦,如有错误,欢迎指正;尚有不足,请多指教~~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。