应用的创建过程
createApp:通过单步调试法可以看到createApp函数
export const createApp = ((...args) => {
// 返回一个renderer(渲染器),渲染器中有createApp方法
const app = ensureRenderer().createApp(...args)
...
return app
}) as CreateAppFunction<Element>
baseCreateRendere:挨个进入函数后发现最后在baseCreateRendere(渲染器函数)中创建渲染器,该函数代码很长,直接拉到2300行
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
...
return { // 渲染器
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
createAppAPI:可以发现app实例就在里面创建的,实例:{use(){}, component(){},....}
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {...},
use(plugin: Plugin, ...options: any[]) {...},
mixin(mixin: ComponentOptions) {...},
component(name: string, component?: Component): any {...},
directive(name: string, directive?: Directive) {...},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {...},
unmount() {...},
provide(key, value) {...)
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
挂载过程
mount:会创建VNode传入render函数中
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
) // 创建VNode
vnode.appContext = context
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) { // isHydrate为null
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG) // 执行render
}
...
},
问题一:mount参数类型问题
我们可以看到mount(rootContainer: HostElement,...)接收的第一个参数的元素类型,但是平时在开发中,允许通过app.mount("#app")的方式传入字符串,那这是什么原因呢?
其实在createApp函数中对mount进行了重写,重写主要是考虑跨平台
export const createApp = ((...args) => {
// 返回一个renderer
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
const { mount } = app // 保存原先的mount方法
// 对mount进行重写
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// normalizeContainer方法就是在web端获取我们的元素,比如div#app
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
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
}
}
}
}
// 先清空container中的原本内容
container.innerHTML = ''
// 调用真正的mount函数,进行挂载
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>
render:由于在mount中创建了vnode,所以走patch函数,详细写在注释中了
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) { // 如果vnode为null,进行卸载操作
if (container._vnode) {
unmount(container._vnode, null, null, true) // 调用unmount函数进行卸载操作
}
} else { // 如果vnode不为null,说明此时需要进行挂在或者更新(由于在mount中传递了vnode,所以走patch函数)
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
// 将新vnode赋值到container上,下次渲染的时候,container._vnode就是oldVnode,那么就会进行更新操作。
container._vnode = vnode
}
patch:根据虚拟dom的类型进行不同的处理(首次传入的是组件,所以执行的是processComponent函数)
const patch: PatchFn = (
n1, // n1是oldVNode
n2, // n2是newVNode
container, // 渲染后会将vnode渲染到conoiner上
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
}
// 根据 n2 Vnode 的类型进行不同的处理
const { type, ref, shapeFlag } = n2
switch (type) {
// 如果当前的 vnode 是文本节点的话,使用 processText 进行处理
case Text:
processText(n1, n2, container, anchor)
break
// 如果当前的 vnode 是注释节点的话,使用 processCommentNode 进行处理
case Comment:
processCommentNode(n1, n2, container, anchor)
break
// 如果当前的 vnode 是静态节点的话,调用 mountStaticNode 和 patchStaticNode 进行处理
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
// 如果节点是 Fragment 的话,使用 processFragment 进行处理
// Fragment 节点的作用是使组件拥有多个根节点
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理元素节点
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} 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__) {...}
}
processComponent:首次创建走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
if (n1 == null) { // n1等于null,表示挂载节点(首次创建n1为null)
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent( // 所以第一次进入mountComponent
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {// n1不为null,那么就更新组件
updateComponent(n1, n2, optimized)
}
}
mountComponent:中进行如下三步
createComponentInstance:创建instance实例,但是所有api都为nullsetupComponent:对组件实例初始化,进行操作和赋值的代码。在另外一篇文章中有详细讲解 [Vue3源码剖析(二)]组件实例创建过程setupRenderEffect:调用设置和渲染有副作用的函数
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
// 1.调用createComponentInstance创建组件实例(创建出来的实例,实例中的属性都是null)
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
...
// 2.对组件实例的props/slot/data等进行初始化处理
// 对所有数据进行操作和赋值的代码
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
...
// 3.调用设置和渲染有副作用的函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
setupRenderEffect:effect作用是当组件的数据发生变化时,effect函数包裹就会执行
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
// 组件没有挂载,就进行一个挂载操作
if (!instance.isMounted) {
...
// 调用renderComponentRoot渲染组件,生成子树的vnode
// 因为template会被编译成render函数,所以renderComponentRoot就是去执行render函数获取对应的vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
...
// 进行patch的递归过程
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
...
instance.isMounted = true // 之后就走更新
...
}
// 更新组件
else {...}
此时又回到了patch函数(patch函数在上面)中,在vue3中是template内允许有多个根,如果多个根就会执行processFragment,单个根就会执行processElement。实际上这两个函数的逻辑都差不多,以下就看processElement函数吧。
processElement:可以看出我们是第一次挂载那么执行mountElement
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
// 第一次挂载操作
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 有旧node进行更新操作
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
mountElement:如果该元素没有子元素,只有文本节点就直接渲染文本。有子元素就进入mountChildren
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, dirs } = vnode
// 根据类型和其他属性,创建DOM元素节点
// hostCreateElement是跨平台的函数,在web下会执行document.createElement函数
// 所以最终不管是vue还是react,他们都会操作dom,只是对我们都是不可见的。
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// 如果子节点是纯文本的情况
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
// 如果子节点是一个数组的情况
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
...
}
mountChildren:会遍历所有的子节点,挨个进行patch
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
// 对子节点进行遍历挨个进行patch
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
总结
以下是挂载的整个流程