前言
本篇文章是分析Vue3.2 DOM初始化挂载流程,分析在vue初始化流程之后的第一次挂载,如果想要了解初始化流程可以先去看一下vue3.2 初始化的时候做了什么?这篇文章。
在挂载的流程中,可能会遇到组件,vue把这些东西统一归为 "资产",指令、组件还有vue2的过滤器都被认为是资产,在本文中只会讲到组件,其他的后面的文章会一步步分析。
DOM初始化挂载
继上次vue初始化流程,到最后会执行componentUpdateFn
函数,在这个函数开头执行了三个beforeMount
,分别是v3的beforeMount
生命周期选项、onVnodeBeforeMount
生命周期函数、v2的hook:beforeMount
事件函数。但是这些都不是重点。
if
分支是服务端渲染,我们这里是客户端渲染,所有走else
分支,在第1533行通过template
编译后的render
渲染函数产生了最初的vnode
,交给了patch
函数去解析,在初始化流程中,一定是没有旧的vnode
。走的都是挂载。
这里先提前说一下这些参数分别是什么,n1
是旧的vnode
、n2
是新的vnode
、container
是当前vnode
宿主元素、anchor
是挂载元素时的瞄点、parentComponent
是当前vnode
父组件,没有就是null
、parentSuspense
是包着当前vnode
最近的内置组件<Suspense>
,没有就是null
、isSVG
:当前节点是SVG
吗、slotScopeIds
是插槽作用id,optimeized
是优化模式还是非优化模式,
只有一个根元素
前面三步,在初始化流程中绝对不会执行的,App
根组件已经解析过了,现在解析应该是第一个节点,看下面的这个结构。
const App = {
template: `
<div id="app">
<div>{{msg}}</div>
</div>
`,
setup() {
const msg = ref('vue3初始化挂载')
return {
msg
}
}
}
最外层的id
是app
的div
是宿主元素,解析的是他的第一个子元素,在上面的案例中,第一个是一个div
,走解析元素的分支,执行processElement
没有旧节点,走mountElement
分支,在里面会根据节点类型创建一个新元素。
这里会有两种情况,先说第一种比较简单,当前节点的子节点只是一个字符串,插入就可以,接着往后执行,后的就是和有关指令、props之类,我们没有使用这里也就不关心,再往后就是将元素插入到容器中,这里的容器是宿主元素app
,执行完这个页面中就可以看到了,最后回到patch
函数中,patch
是直接结束了,整个流程结束了。
如果这个节点有多个子节点,就会走mountChild
分支,遍历每一个子节点再次调用patch。所以总体流程如下:
有多个根元素
v2不允许用户在一个模板写多个根元素,只允许有一个,v3允许有多个根元素,看下面的这个例子:
const App = {
template: `
<div>{{counter}}</div>
<button @click="addCounter"></button>
`,
setup() {/* code... */}
}
如果有多个同级别的节点,且不把它们包在同一个节点中,就会产生一个片段(Fragment
),在patch
函数中他就会走processFragment
分支,解析一个片段。转换成渲染函数,会看到vue给这个代码片段加了一个抽象的父节点Fragment
,这其实vue自己内部实现的。
fragmentStartAnchor
和fragmentEndAnchor
是作为这个代码片段的开始点和结束点,在初始化时这两个东西时不存在的,会创建两个空字符串作为这两个东西。
在开始解析Fragment
子元素之前,会将两个瞄点先插入到容器中,在上面的例子中会插入的宿主元素app
中,下面就开始使用mountChildren
解析Fragment
子元素,最后还是再次调用patch
函数。总体流程如下,
静态节点、文本节点、注释节点
有如下模板:
const App = {
template: `
<!-- vue3初始化挂载 -->
<div id="1">
<div id="1">
<div id="1">
<div id="1">
<div id="1"></div>
</div>
</div>
</div>
</div>
<div :class="foo"></div>
vue3初始化挂载
`
}
- 注释节点
我们先来看注释节点是如何解析的,在patch
函数中走的processCommentNode
,在prcessCommentNode
内部就是简单的创建一个注释插入的父容器中就可以了。
- 文本节点
文本节点在patch
函数中走processText
,在prcessCommentNode
内部就是简单的创建一个文本插入的父容器中就可以了。
- 静态节点
一个vue树中,如果静态提升的VNode
或者整个树都是静态的,vue会将其优化,进行预字符串化,而静态节点就是将字符串化之后再通过createStaticVNode
创建的,在patch
函数中会走mountStaticNode
,mountStaticNode
调用insertStaticContent
去追加节点到容器中
创建一个template
,将字符串化的结构放进去,这个template
中的content
属性就会是一个fragment-document
,拿出来放入父容器中。字符串结构会缓存,如果下次还是这种就可以直接使用了。
Teleport
和Suspense
的解析流程可以去看我写的Vue3 新特性 Teleport Suspense实现原理,这篇文章详细的说明了Teleport
和Suspense
初始化流程和更新流程。
组件的挂载
假设有如下结构:
<div>
<Foo></Foo>
</div>
组件在解析之前会走一次定义的流程,在渲染函数中可以很清楚的看到组件交给一个函数去解析,转换成渲染函数之后交给了resolveComponent
函数,但是这个函数是去调用了另外一个函数:resolveAssets
函数。
函数开头,必须要存在实例,不然直接报警告然后结束,如果设置了name
选项的组件有最高优先级,在校验和传递进来的name
值相同可直接返回Component
,Componnet
是组件配置选项。
这里有两个name
,一个是options api
中的选项,这个很直接设置是啥就是啥。一个是经过complie
编译之后的产生的,这会和用户在模板中如何写组件标签有关,举个例子:如果是:<FooterButton></FooterButton>
,就会变成 FooterButton
,如果是:<footer-button></footer-button>
,就会变成 footer-button
,所以校验时候会进行多种字符串转换。
这里与其说是注册,其实就是去options api
选项中找到对应的"资产",举个例子,这里在解析组件,需要找到组件的配置,会去当前渲染实例的components
选项中找到"组件资产",如果有正常返回没有就会去全局上下文中找,还是没有那就只能返回false
。
或许是渲染自己,前提是没有找到"资产"和maybeSelfReference
为true
,即参照自己渲染,什么都没有就只能报错。但是如果是动态组件则一定不会参照自己,最后,如果一切正常且不参照自己,会被找到的资产返回去。
当前一切执行完毕之后,会把组件开始转换成VNode
,后面流程就是渲染。
寻找资产
registery
是组件实例,name
就是资产的名称,是个字符串,由于名称的有多种写法,这里会进行多种转换资产名称以找到资产。
总结
vue在挂载dom的流程中,是深度优先的规则,遇到一个节点,会一直如果有子节点,会递归调用patch
去解析,直到最里面的子节点解析完毕才会到下一个节点。当一个节点被解析完毕,在mountElement
中执行hostInsert
在页面中就可以看到这个节点存在,如果是静态节点(是元素节点的子节点)、文本节点、注释节点是直接通过histInert
追加到页面中,如果只是静态节点,在执行完insertStaticContent
就会在页面中看到元素。
vue的初始化流程到这里就分析完毕了,如果缺少或不对的,希望哥哥们可以指出和在评论区补充。谢谢!