Vue3將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁初次渲染詳解
正文
在上一篇中,只講了大致的執(zhí)行流程,其中有關(guān)渲染部分的內(nèi)容并沒有深入,而這部分關(guān)系到創(chuàng)建Vnode和渲染Vnode的過程,就是將代碼通過渲染變成大家可見的網(wǎng)頁畫面的這一部分內(nèi)容。 createApp函數(shù)內(nèi)部的app.mount方法是一個標(biāo)準(zhǔn)的可跨平臺的組件渲染流程:先創(chuàng)建VNode,再渲染VNode。

何時會進(jìn)行虛擬函數(shù)的創(chuàng)建和渲染?
vue3初始化過程中,createApp()指向的源碼 core/packages/runtime-core/src/apiCreateApp.ts中

export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,//由之前的baseCreateRenderer中的render傳入
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {//rootComponent根組件
let isMounted = false
//生成一個具體的對象,提供對應(yīng)的API和相關(guān)屬性
const app: App = (context.app = {//將以下參數(shù)傳入到context中的app里
//...省略其他邏輯處理
//掛載
mount(
rootContainer: HostElement,
isHydrate?: boolean,//是用來判斷是否用于服務(wù)器渲染,這里不講所以省略
isSVG?: boolean
): any {
//如果處于未掛載完畢狀態(tài)下運(yùn)行
if (!isMounted) {
//創(chuàng)建一個新的虛擬節(jié)點(diǎn)傳入根組件和根屬性
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 存儲app上下文到根虛擬節(jié)點(diǎn),這將在初始掛載時設(shè)置在根實(shí)例上。
vnode.appContext = context
}
//渲染虛擬節(jié)點(diǎn),根容器
render(vnode, rootContainer, isSVG)
isMounted = true //將狀態(tài)改變成為已掛載
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return getExposeProxy(vnode.component!) || vnode.component!.proxy
}},
})
return app
}
}
在mount的過程中,當(dāng)運(yùn)行處于未掛載時, const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)創(chuàng)建虛擬節(jié)點(diǎn)并且將 vnode(虛擬節(jié)點(diǎn))、rootContainer(根容器),isSVG作為參數(shù)傳入render函數(shù)中去進(jìn)行渲染。
什么是VNode?
虛擬節(jié)點(diǎn)其實(shí)就是JavaScript的一個對象,用來描述DOM。
這里可以編寫一個實(shí)際的簡單例子來輔助理解,下面是一段html的普通元素節(jié)點(diǎn)
<div class="title" style="font-size:16px;width=100px">這是一個標(biāo)題</div>
如何用虛擬節(jié)點(diǎn)來表示?
const VNode ={
type:'div',
props:{
class:'title',
style:{
fontSize:'16px',
width:'100px'
}
},
children:'這是一個標(biāo)題',
key:null
}
這里官方文檔給出了建議:完整的 VNode 接口包含其他內(nèi)部屬性,但是強(qiáng)烈建議避免使用這些沒有在這里列舉出的屬性。這樣能夠避免因內(nèi)部屬性變更而導(dǎo)致的不兼容性問題。
vue3對vnode的type做了更詳細(xì)的分類。在創(chuàng)建vnode之前先了解一下shapeFlags,這個類對type的類型信息做了對應(yīng)的編碼。以便之后在patch階段,可以通過不同的類型執(zhí)行對應(yīng)的邏輯處理。同時也能看到type有元素,方法函數(shù)組件,帶狀態(tài)的組件,子類是文本等。
前置須知
ShapeFlags
// package/shared/src/shapeFlags.ts
//這是一個ts的枚舉類,從中也能了解到虛擬節(jié)點(diǎn)的類型
export const enum ShapeFlags {
//DOM元素 HTML
ELEMENT = 1,
//函數(shù)式組件
FUNCTIONAL_COMPONENT = 1 << 1, //2
//帶狀態(tài)的組件
STATEFUL_COMPONENT = 1 << 2,//4
//子節(jié)點(diǎn)是文本
TEXT_CHILDREN = 1 << 3,//8
//子節(jié)點(diǎn)是數(shù)組
ARRAY_CHILDREN = 1 << 4,//16
//子節(jié)點(diǎn)帶有插槽
SLOTS_CHILDREN = 1 << 5,//32
//傳送,將一個組件內(nèi)部的模板‘傳送'到該組件DOM結(jié)構(gòu)外層中去,例如遮罩層的使用
TELEPORT = 1 << 6,//64
//懸念,用于等待異步組件時渲染一些額外的內(nèi)容,比如骨架屏,不過目前是實(shí)驗(yàn)性功能
SUSPENSE = 1 << 7,//128
//要緩存的組件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,//256
//已緩存的組件
COMPONENT_KEPT_ALIVE = 1 << 9,//512
//組件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}//4 | 2
它用來表示當(dāng)前虛擬節(jié)點(diǎn)的類型。我們可以通過對shapeFlag做二進(jìn)制運(yùn)算來描述當(dāng)前節(jié)點(diǎn)的本身是什么類型、子節(jié)點(diǎn)是什么類型。
為什么要使用Vnode?
因?yàn)関node可以抽象,把渲染的過程抽象化,使組件的抽象能力也得到提升。 然后因?yàn)関ue需要可以跨平臺,講節(jié)點(diǎn)抽象化后可以通過平臺自己的實(shí)現(xiàn),使之在各個平臺上渲染更容易。 不過同時需要注意的一點(diǎn),雖然使用的是vnode,但是這并不意味著vnode的性能更具有優(yōu)勢。比如很大的組件,是表格上千行的表格,在render過程中,創(chuàng)建vnode勢必得遍歷上千次vnode的創(chuàng)建,然后遍歷上千次的patch,在更新表格數(shù)據(jù)中,勢必會出現(xiàn)卡頓的情況。即便是在patch中使用diff優(yōu)化了對DOM操作次數(shù),但是始終需要操作。
Vnode是如何創(chuàng)建的?
vue3 提供了一個 h() 函數(shù)用于創(chuàng)建 vnodes:
import {h} from 'vue'
h('div', { id: 'foo' })
其本質(zhì)也是調(diào)用 createVNode()函數(shù)。
const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)
createVNode()位于 core/packages/runtime-core/src/vnode.ts
//創(chuàng)建虛擬節(jié)點(diǎn)
export const createVNode = ( _createVNode) as typeof _createVNode
function _createVNode(
//標(biāo)簽類型
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
//數(shù)據(jù)和vnode的屬性
props: (Data & VNodeProps) | null = null,
//子節(jié)點(diǎn)
children: unknown = null,
//patch標(biāo)記
patchFlag: number = 0,
//動態(tài)參數(shù)
dynamicProps: string[] | null = null,
//是否是block節(jié)點(diǎn)
isBlockNode = false
): VNode {
//內(nèi)部邏輯處理
//使用更基層的createBaseVNode對各項(xiàng)參數(shù)進(jìn)行處理
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
剛才省略的內(nèi)部邏輯處理,這里去除了只有在開發(fā)環(huán)境下才運(yùn)行的代碼:
先是判斷
if (isVNode(type)) {
//創(chuàng)建虛擬節(jié)點(diǎn)接收到已存在的節(jié)點(diǎn),這種情況發(fā)生在諸如 <component :is="vnode"/>
// #2078 確保在克隆過程中合并refs,而不是覆蓋它。
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
//如果擁有子節(jié)點(diǎn),將子節(jié)點(diǎn)規(guī)范化處理
if (children) {normalizeChildren(cloned, children)}:
//將拷貝的對象存入currentBlock中
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
//返回克隆
return cloned
}
// 類組件規(guī)范化
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 類(class)和風(fēng)格(style) 規(guī)范化.
if (props) {
//對于響應(yīng)式或者代理的對象,我們需要克隆來處理,以防止觸發(fā)響應(yīng)式和代理的變動
props = guardReactiveProps(props)!
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
// 響應(yīng)式對象需要克隆后再處理,以免觸發(fā)響應(yīng)式。
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
與之前的shapeFlags枚舉類結(jié)合,將定好的編碼賦值給shapeFlag
// 將虛擬節(jié)點(diǎn)的類型信息編碼成一個位圖(bitmap)
// 根據(jù)type類型來確定shapeFlag的屬性值
const shapeFlag = isString(type)//是否是字符串
? ShapeFlags.ELEMENT//傳值1
: __FEATURE_SUSPENSE__ && isSuspense(type)//是否是懸念類型
? ShapeFlags.SUSPENSE//傳值128
: isTeleport(type)//是否是傳送類型
? ShapeFlags.TELEPORT//傳值64
: isObject(type)//是否是對象類型
? ShapeFlags.STATEFUL_COMPONENT//傳值4
: isFunction(type)//是否是方法類型
? ShapeFlags.FUNCTIONAL_COMPONENT//傳值2
: 0//都不是以上類型 傳值0
以上,將虛擬節(jié)點(diǎn)其中一部分的屬性處理好之后,再傳入創(chuàng)建基礎(chǔ)虛擬節(jié)點(diǎn)函數(shù)中,做更進(jìn)一步和更詳細(xì)的屬性對象創(chuàng)建。
createBaseVNode 虛擬節(jié)點(diǎn)初始化創(chuàng)建
創(chuàng)建基礎(chǔ)虛擬節(jié)點(diǎn)(JavaScript對象),初始化封裝一系列相關(guān)的屬性。
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,//虛擬節(jié)點(diǎn)類型
props: (Data & VNodeProps) | null = null,//內(nèi)部的屬性
children: unknown = null,//子節(jié)點(diǎn)內(nèi)容
patchFlag = 0,//patch標(biāo)記
dynamicProps: string[] | null = null,//動態(tài)參數(shù)內(nèi)容
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,//節(jié)點(diǎn)類型的信息編碼
isBlockNode = false,//是否塊節(jié)點(diǎn)
needFullChildrenNormalization = false
) {
//聲明一個vnode對象,并且將各種屬性賦值,從而完成虛擬節(jié)點(diǎn)的初始化創(chuàng)建
const vnode = {
__v_isVNode: true,//內(nèi)部屬性表示為Vnode
__v_skip: true,//表示跳過響應(yīng)式轉(zhuǎn)換
type, //虛擬節(jié)點(diǎn)類型
props,//虛擬節(jié)點(diǎn)內(nèi)的屬性和props
key: props && normalizeKey(props),//虛擬階段的key用于diff
ref: props && normalizeRef(props),//引用
scopeId: currentScopeId,//作用域id
slotScopeIds: null,//插槽id
children,//子節(jié)點(diǎn)內(nèi)容,樹形結(jié)構(gòu)
component: null,//組件
suspense: null,//傳送組件
ssContent: null,
ssFallback: null,
dirs: null,//目錄
transition: null,//內(nèi)置組件相關(guān)字段
el: null,//vnode實(shí)際被轉(zhuǎn)換為dom元素的時候產(chǎn)生的元素,宿主
anchor: null,//錨點(diǎn)
target: null,//目標(biāo)
targetAnchor: null,//目標(biāo)錨點(diǎn)
staticCount: 0,//靜態(tài)節(jié)點(diǎn)數(shù)
shapeFlag,//shape標(biāo)記
patchFlag,//patch標(biāo)記
dynamicProps,//動態(tài)參數(shù)
dynamicChildren: null,//動態(tài)子節(jié)點(diǎn)
appContext: null,//app上下文
ctx: currentRenderingInstance
} as VNode
//關(guān)于子節(jié)點(diǎn)和block節(jié)點(diǎn)的標(biāo)準(zhǔn)化和信息編碼處理
return vnode
}
由此可見,創(chuàng)建vnode就是一個對props中的內(nèi)容進(jìn)行標(biāo)準(zhǔn)化處理,然后對節(jié)點(diǎn)類型進(jìn)行信息編碼,對子節(jié)點(diǎn)的標(biāo)準(zhǔn)化處理和類型信息編碼,最后創(chuàng)建vnode對象的過程。
render 渲染 VNode
baseCreateRenderer()返回對象中,有render()函數(shù),hydrate用于服務(wù)器渲染和createApp函數(shù)的。 在baseCreateRenderer()函數(shù)中,定義了render()函數(shù),render的內(nèi)容不復(fù)雜。
組件在首次掛載,以及后續(xù)的更新等,都會觸發(fā)mount(),而這些,其實(shí)都會調(diào)用render()渲染函數(shù)。render()會先判斷vnode虛擬節(jié)點(diǎn)是否存在,如果不存在進(jìn)行unmount()卸載操作。 如果存在則會調(diào)用patch()函數(shù)。因此可以推測,patch()的過程中,有關(guān)組件相關(guān)處理。

const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {//判斷是否傳入虛擬節(jié)點(diǎn),如果節(jié)點(diǎn)不存在則運(yùn)行
if (container._vnode) {//判斷容器中是否已有節(jié)點(diǎn)
unmount(container._vnode, null, null, true)//如果已有節(jié)點(diǎn)則卸載當(dāng)前節(jié)點(diǎn)
}
} else {
//如果節(jié)點(diǎn)存在,則調(diào)用patch函數(shù),從參數(shù)看,會傳入新舊節(jié)點(diǎn)和容器
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPreFlushCbs() //組件更新前的回調(diào)
flushPostFlushCbs()//組件更新后的回調(diào)
container._vnode = vnode//將虛擬節(jié)點(diǎn)賦值到容器上
}
patch VNode
這里來看一下有關(guān)patch()函數(shù)的代碼,側(cè)重了解當(dāng)組件初次渲染的時候的流程。

// 注意:此閉包中的函數(shù)應(yīng)使用 'const xxx = () => {}'樣式,以防止被小寫器內(nèi)聯(lián)。
// patch:進(jìn)行diff算法,crateApp->vnode->element
const patch: PatchFn = (
n1,//老節(jié)點(diǎn)
n2,//新節(jié)點(diǎn)
container,//宿主元素 container
anchor = null,//錨點(diǎn),用來標(biāo)識當(dāng)我們對新舊節(jié)點(diǎn)做增刪或移動等操作時,以哪個節(jié)點(diǎn)為參照物
parentComponent = null,//父組件
parentSuspense = null,//父懸念
isSVG = false,
slotScopeIds = null,//插槽
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {// 如果新老節(jié)點(diǎn)相同則停止
return
}
// 打補(bǔ)丁且不是相同類型,則卸載舊節(jié)點(diǎn),錨點(diǎn)后移
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null //n1復(fù)位
}
//是否動態(tài)節(jié)點(diǎn)優(yōu)化
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
//結(jié)構(gòu)n2新節(jié)點(diǎn),獲取新節(jié)點(diǎn)的類型
const { type, ref, shapeFlag } = n2
switch (type) {
case Text: //文本類
processText(n1, n2, container, anchor)//文本節(jié)點(diǎn)處理
break
case Comment://注釋類
processCommentNode(n1, n2, container, anchor)//處理注釋節(jié)點(diǎn)
break
case Static://靜態(tài)類
if (n1 == null) {//如果老節(jié)點(diǎn)不存在
mountStaticNode(n2, container, anchor, isSVG)//掛載靜態(tài)節(jié)點(diǎn)
}
break
case Fragment://片段類
processFragment(
//進(jìn)行片段處理
)
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) {
;(type as typeof TeleportImpl).process(
// 如果類型是傳送,進(jìn)行處理
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
//懸念處理
)
}
}
// 設(shè)置 參考 ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
patch函數(shù)可見,主要做的就是 新舊虛擬節(jié)點(diǎn)之間的對比,這也是常說的diff算法,結(jié)合render(vnode, rootContainer, isSVG)可以看出vnode對應(yīng)的是n1也就是新節(jié)點(diǎn),而rootContainer對應(yīng)n2,也就是老節(jié)點(diǎn)。其做的邏輯判斷是。
- 新舊節(jié)點(diǎn)相同則直接返回
- 舊節(jié)點(diǎn)存在,且新節(jié)點(diǎn)和舊節(jié)點(diǎn)的類型不同,舊節(jié)點(diǎn)將被卸載
unmount且復(fù)位清空null。錨點(diǎn)移向下個節(jié)點(diǎn)。 - 新節(jié)點(diǎn)是否是動態(tài)值優(yōu)化標(biāo)記
- 對新節(jié)點(diǎn)的類型判斷
- 文本類:
processText - 注釋類:
processComment - 靜態(tài)類:
mountStaticNode - 片段類:
processFragment - 默認(rèn)
- 文本類:
而這個默認(rèn)才是主要的部分也是最常用到的部分。里面包含了對類型是元素element、組件component、傳送teleport、懸念suspense的處理。這次主要講的是虛擬節(jié)點(diǎn)到組件和普通元素渲染的過程,其他類型的暫時不提,內(nèi)容展開過于雜亂。
實(shí)際上第一次初始運(yùn)行的時候,patch判斷vnode類型根節(jié)點(diǎn),因?yàn)関ue3書寫的時候,都是以組件的形式體現(xiàn),所以第一次的類型勢必是component類型。

processComponent 節(jié)點(diǎn)類型是組件下的處理
const processComponent = (
n1: VNode | null,//老節(jié)點(diǎn)
n2: VNode,//新節(jié)點(diǎn)
container: RendererElement,//宿主
anchor: RendererNode | null,//錨點(diǎn)
parentComponent: ComponentInternalInstance | null,//父組件
parentSuspense: SuspenseBoundary | null,//父懸念
isSVG: boolean,
slotScopeIds: string[] | null,//插槽
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {//如果老節(jié)點(diǎn)不存在,初次渲染的時候
//省略一部分n2其他情況下的處理
//掛載組件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
//更新組件
updateComponent(n1, n2, optimized)
}
}
老節(jié)點(diǎn)n1不存在null的時候,將掛載n2節(jié)點(diǎn)。如果老節(jié)點(diǎn)存在的時候,則更新組件。因此mountComponent()最常見的就是在首次渲染的時候,那時舊節(jié)點(diǎn)都是空的。
接下來就是看如何掛載組件mountComponent()
const mountComponent: MountComponentFn = (
initialVNode,//對應(yīng)n2 新的節(jié)點(diǎn)
container,//對應(yīng)宿主
anchor,//錨點(diǎn)
parentComponent,//父組件
parentSuspense,//父傳送
isSVG,//是否SVG
optimized//是否優(yōu)化
) => {
// 2.x編譯器可以在實(shí)際安裝前預(yù)先創(chuàng)建組件實(shí)例。
const compatMountInstance =
//判斷是不是根組件且是組件
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
//創(chuàng)建組件實(shí)例
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 如果新節(jié)點(diǎn)是緩存組件的話那么將internals賦值給期渲染函數(shù)
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 為了設(shè)置上下文處理props和slot插槽
if (!(__COMPAT__ && compatMountInstance)) {
//設(shè)置組件實(shí)例
setupComponent(instance)
}
//setup()是異步的。這個組件在進(jìn)行之前依賴于異步邏輯的解決
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
if (!initialVNode.el) {//如果n2沒有宿主
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
return
}
//設(shè)置運(yùn)行渲染副作用函數(shù)
setupRenderEffect(
instance,//存儲了新節(jié)點(diǎn)的組件上下文,props插槽等其他實(shí)例屬性
initialVNode,//新節(jié)點(diǎn)n2
container,//容器
anchor,//錨點(diǎn)
parentSuspense,//父懸念
isSVG,//是否SVG
optimized//是否優(yōu)化
)
}
掛載組件中,除開緩存和懸掛上的函數(shù)處理,其邏輯上基本為:創(chuàng)建組件的實(shí)例createComponentInstance(),設(shè)置組件實(shí)例 setupComponent(instance)和設(shè)置運(yùn)行渲染副作用函數(shù)setupRenderEffect()。
創(chuàng)建組件實(shí)例,基本跟創(chuàng)建虛擬節(jié)點(diǎn)一樣的,內(nèi)部以對象的方式創(chuàng)建渲染組件實(shí)例。 設(shè)置組件實(shí)例,是將組件中許多數(shù)據(jù),賦值給了instance,維護(hù)組件上下文,同時對props和插槽等屬性初始化處理。
然后是setupRenderEffect 設(shè)置渲染副作用函數(shù);
const setupRenderEffect: SetupRenderEffectFn = (
instance,//實(shí)例
initialVNode,//初始化節(jié)點(diǎn)
container,//容器
anchor,//錨點(diǎn)
parentSuspense,//父懸念
isSVG,//是否是SVG
optimized//優(yōu)化標(biāo)記
) => {
//組件更新方法
const componentUpdateFn = () => {
//如果組件處于未掛載的狀態(tài)下
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
//解構(gòu)
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// 掛載前的鉤子
// 掛載前的節(jié)點(diǎn)
toggleRecurse(instance, true)
//這部分是跟服務(wù)器渲染相關(guān)的邏輯處理
//創(chuàng)建子樹,同時
const subTree = (instance.subTree = renderComponentRoot(instance))
//遞歸
patch(
null,//因?yàn)槭菕燧d,所以n1這個老節(jié)點(diǎn)是空的。
subTree,//子樹賦值到n2這個新節(jié)點(diǎn)
container,//掛載到container上
anchor,
instance,
parentSuspense,
isSVG
)
//保留渲染生成的子樹DOM節(jié)點(diǎn)
initialVNode.el = subTree.el
// 已掛載鉤子
// 掛在后的節(jié)點(diǎn)
//激活為了緩存根的鉤子
// #1742 激活的鉤子必須在第一次渲染后被訪問 因?yàn)樵撱^子可能會被子類的keep-alive注入。
instance.isMounted = true
// #2458: deference mount-only object parameters to prevent memleaks
// #2458: 遵從只掛載對象的參數(shù)以防止內(nèi)存泄漏
initialVNode = container = anchor = null as any
} else {
// 更新組件
// 這是由組件自身狀態(tài)的突變觸發(fā)的(next: null)?;蛘吒讣壵{(diào)用processComponent(下一個:VNode)。
}
}
// 創(chuàng)建用于渲染的響應(yīng)式副作用
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // 在組件的效果范圍內(nèi)跟蹤它
))
//更新方法
const update: SchedulerJob = (instance.update = () => effect.run())
//實(shí)例的uid賦值給更新的id
update.id = instance.uid
// 允許遞歸
// #1801, #2043 組件渲染效果應(yīng)允許遞歸更新
toggleRecurse(instance, true)
update()
}
setupRenderEffect() 最后執(zhí)行的了 update()方法,其實(shí)是運(yùn)行了effect.run(),并且將其賦值給了instance.updata中。而 effect 涉及到了 vue3 的響應(yīng)式模塊,該模塊的主要功能就是,讓對象屬性具有響應(yīng)式功能,當(dāng)其中的屬性發(fā)生了變動,那effect副作用所包含的函數(shù)也會重新執(zhí)行一遍,從而讓界面重新渲染。這一塊內(nèi)容先不管。從effect函數(shù)看,明白了調(diào)用了componentUpdateFn, 即組件更新方法,這個方法涉及了2個條件,一個是初次運(yùn)行的掛載,而另一個是節(jié)點(diǎn)變動后的更新組件。 componentUpdateFn中進(jìn)行的初次渲染,主要是生成了subTree然后把subTree傳遞到patch進(jìn)行了遞歸掛載到container上。
subTree是什么?
subTree也是一個vnode對象,然而這里的subTree和initialVNode是不同的。以下面舉個例子:
<template> <div class="app"> <p>title</p> <helloWorld> </div> </template>
而helloWorld組件中是<div>標(biāo)簽包含一個<p>標(biāo)簽
<template> <div class="hello"> <p>hello world</p> </div> </template>
在App組件中,<helloWorld> 節(jié)點(diǎn)渲染渲染生成的vnode就是 helloWorld組件的initialVNode,而這個組件內(nèi)部所有的DOM節(jié)點(diǎn)就是vnode通過執(zhí)行renderComponentRoot渲染生成的的subTree。 每個組件渲染的時候都會運(yùn)行render函數(shù),renderComponentRoot就是去執(zhí)行render函數(shù)創(chuàng)建整個組件內(nèi)部的vnode,然后進(jìn)行標(biāo)準(zhǔn)化就得到了該函數(shù)的返回結(jié)果:子樹vnode。 生成子樹后,接下來就是繼續(xù)調(diào)用patch函數(shù)把子樹vnode掛載到container上去。 回到patch后,就會繼續(xù)對子樹vnode進(jìn)行判斷,例如上面的App組件的根節(jié)點(diǎn)是<div>標(biāo)簽,而對應(yīng)的subTree就是普通元素vnode,接下來就是堆普通Element處理的流程。
當(dāng)節(jié)點(diǎn)的類型是普通元素DOM時候,patch判斷運(yùn)行processElement

const processElement = (
n1: VNode | null, //老節(jié)點(diǎn)
n2: VNode,//新節(jié)點(diǎn)
container: RendererElement,//容器
anchor: RendererNode | null,//錨點(diǎn)
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {//如果沒有老節(jié)點(diǎn),其實(shí)就是初次渲染,則運(yùn)行mountElement
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
//如果是更新節(jié)點(diǎn)則運(yùn)行patchElement
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
邏輯依舊,如果有n1老節(jié)點(diǎn)為null的時候,運(yùn)行掛載元素的邏輯,否則運(yùn)行更新元素節(jié)點(diǎn)的方法。
以下是mountElement()的代碼:
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
//創(chuàng)建元素節(jié)點(diǎn)
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// 首先掛載子類,因?yàn)槟承﹑rops依賴于子類內(nèi)容
// 已經(jīng)渲染, 例如 `<select value>`
// 如果標(biāo)記判斷子節(jié)點(diǎn)類型是文本類型
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 處理子節(jié)點(diǎn)是純文本的情況
hostSetElementText(el, vnode.children as string)
//如果標(biāo)記類型是數(shù)組子類
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
//掛載子類,進(jìn)行patch后進(jìn)行掛載
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// 設(shè)置范圍id
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
// props相關(guān)的處理,比如 class,style,event,key等屬性
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {//key值不等于value字符且不是
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ('value' in props) {
hostPatchProp(el, 'value', null, props.value)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
Object.defineProperty(el, '__vueParentComponent', {
value: parentComponent,
enumerable: false
}
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// #1583 對于內(nèi)部懸念+懸念未解決的情況,進(jìn)入鉤子應(yīng)該在懸念解決時調(diào)用。
// #1689 對于內(nèi)部懸念+懸念解決的情況,只需調(diào)用它
const needCallTransitionHooks =
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition && !transition.persisted
if (needCallTransitionHooks) {
transition!.beforeEnter(el)
}
//把創(chuàng)建的元素el掛載到container容器上。
hostInsert(el, container, anchor)
if (
(vnodeHook = props && props.onVnodeMounted) ||
needCallTransitionHooks ||
dirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
mountElement掛載元素主要做了,創(chuàng)建DOM元素節(jié)點(diǎn),處理節(jié)點(diǎn)子節(jié)點(diǎn),掛載子節(jié)點(diǎn),同時對props相關(guān)處理。
所以根據(jù)代碼,首先是通過hostCreateElement方法創(chuàng)建了DOM元素節(jié)點(diǎn)。
const {createElement:hostCreateElement } = options
是從options這個實(shí)參中解構(gòu)并重命名為hostCreateElement方法的,那么這個實(shí)參是從哪里來 需要追溯一下,回到初次渲染開始的流程中去。

從這流程圖可以清楚的知道,options中createElement方法是從nodeOps.ts文件中導(dǎo)出的并傳入baseCreateRender()方法內(nèi)的。
該文件位于:core/packages/runtime-dom/src/nodeOps.ts
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
從中可以看出,其實(shí)是調(diào)用了底層的DOM API document.createElement創(chuàng)建元素。
說回上面,創(chuàng)建完DOM節(jié)點(diǎn)元素之后,接下來是繼續(xù)判斷子節(jié)點(diǎn)的類型,如果子節(jié)點(diǎn)是文本類型的,則調(diào)用處理文本hostSetElementText()方法。
const {setElementText: hostSetElementText} = option
setElementText: (el, text) => {
el.textContent = text
},
與前面的createElement一樣,setElementText方法是通過設(shè)置DOM元素的textContent屬性設(shè)置文本。
而如果子節(jié)點(diǎn)的類型是數(shù)組類,則執(zhí)行mountChildren方法,對子節(jié)點(diǎn)進(jìn)行掛載:
const mountChildren: MountChildrenFn = (
children,//子節(jié)點(diǎn)數(shù)組里的內(nèi)容
container,//容器
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,//優(yōu)化標(biāo)記
start = 0
) => {
//遍歷子節(jié)點(diǎn)中的內(nèi)容
for (let i = start; i < children.length; i++) {
//根據(jù)優(yōu)化標(biāo)記進(jìn)行判斷進(jìn)行克隆或者節(jié)點(diǎn)初始化處理。
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
//執(zhí)行patch方法,遞歸掛載child
patch(
null,//因?yàn)槭浅醮螔燧d所以沒有老的節(jié)點(diǎn)
child,//虛擬子節(jié)點(diǎn)
container,//容器
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
子節(jié)點(diǎn)的掛載邏輯看起來會非常眼熟,在對children數(shù)組進(jìn)行遍歷之后獲取到的每一個child,進(jìn)行預(yù)處理后并對其執(zhí)行掛載方法。 結(jié)合之前調(diào)用mountChildren()方法傳入的實(shí)參和其形參之間的對比。
mountChildren(
vnode.children as VNodeArrayChildren, //節(jié)點(diǎn)中子節(jié)點(diǎn)的內(nèi)容
el,//DOM元素
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
const mountChildren: MountChildrenFn = (
children,//子節(jié)點(diǎn)數(shù)組里的內(nèi)容
container,//容器
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,//優(yōu)化標(biāo)記
start = 0
)
明確的對應(yīng)上了第二個參數(shù)是container,而調(diào)用mountChildren方法時傳入第二個參數(shù)的是在調(diào)用mountElement()時創(chuàng)建的DOM節(jié)點(diǎn),這樣便建立起了父子關(guān)系。 而且,后續(xù)的繼續(xù)遞歸patch(),能深度遍歷樹的方式,可以完整的把DOM樹遍歷出來,完成渲染。
處理完節(jié)點(diǎn)的后,最后會調(diào)用 hostInsert(el, container, anchor)
const {insert: hostInsert} = option
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
再次就用調(diào)用DOM方法將子類的內(nèi)容掛載到parent,也就是把child掛載到parent下,完成節(jié)點(diǎn)的掛載。
注意點(diǎn):node.insertBefore(newnode,existingnode)中_existingnode_雖然是可選的對象,但是實(shí)際上,在不同的瀏覽器會有不同的表現(xiàn)形式,所以如果沒有existingnode值的情況下,填入null會將新的節(jié)點(diǎn)添加到node子節(jié)點(diǎn)的尾部。

以上就是Vue3將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁初次渲染詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue3虛擬節(jié)點(diǎn)渲染網(wǎng)頁的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vuejs數(shù)據(jù)超出單行顯示更多,點(diǎn)擊展開剩余數(shù)據(jù)實(shí)例
這篇文章主要介紹了vuejs數(shù)據(jù)超出單行顯示更多,點(diǎn)擊展開剩余數(shù)據(jù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
vue中使用elementui實(shí)現(xiàn)樹組件tree右鍵增刪改功能
這篇文章主要介紹了vue中使用elementui實(shí)現(xiàn)對樹組件tree右鍵增刪改功能,右擊節(jié)點(diǎn)可進(jìn)行增刪改,對節(jié)點(diǎn)數(shù)據(jù)進(jìn)行模糊查詢功能,本文給大家分享了完整代碼,需要的朋友可以參考下2022-08-08
vue.js組件vue-waterfall-easy實(shí)現(xiàn)瀑布流效果
這篇文章主要為大家詳細(xì)介紹了vue.js實(shí)現(xiàn)瀑布流之vue-waterfall-easy的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
vue使用element-resize-detector監(jiān)聽元素寬度變化方式
這篇文章主要介紹了vue使用element-resize-detector監(jiān)聽元素寬度變化方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
詳解vue-cli3開發(fā)Chrome插件實(shí)踐
這篇文章主要介紹了vue-cli3開發(fā)Chrome插件實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
vue中nextTick函數(shù)和react類似實(shí)現(xiàn)代碼
Vue 3 中的 nextTick 主要通過 Promise 實(shí)現(xiàn)異步調(diào)度,返回一個 Promise 對象,這篇文章主要介紹了vue中nextTick函數(shù)和react類似實(shí)現(xiàn)代碼,需要的朋友可以參考下2024-04-04
詳解用vue-cli來搭建vue項(xiàng)目和webpack
本篇文章主要介紹了詳解用vue-cli來搭建vue項(xiàng)目和webpack,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04

