您好,登錄后才能下訂單哦!
這篇文章主要介紹了Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。
createApp函數(shù)內(nèi)部的app.mount方法是一個(gè)標(biāo)準(zhǔn)的可跨平臺(tái)的組件渲染流程:先創(chuàng)建VNode,再渲染VNode。
vue3初始化過(guò)程中,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 //生成一個(gè)具體的對(duì)象,提供對(duì)應(yīng)的API和相關(guān)屬性 const app: App = (context.app = {//將以下參數(shù)傳入到context中的app里 //...省略其他邏輯處理 //掛載 mount( rootContainer: HostElement, isHydrate?: boolean,//是用來(lái)判斷是否用于服務(wù)器渲染,這里不講所以省略 isSVG?: boolean ): any { //如果處于未掛載完畢狀態(tài)下運(yùn)行 if (!isMounted) { //創(chuàng)建一個(gè)新的虛擬節(jié)點(diǎn)傳入根組件和根屬性 const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // 存儲(chǔ)app上下文到根虛擬節(jié)點(diǎn),這將在初始掛載時(shí)設(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的過(guò)程中,當(dāng)運(yùn)行處于未掛載時(shí), const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)創(chuàng)建虛擬節(jié)點(diǎn)并且將 vnode(虛擬節(jié)點(diǎn))、rootContainer(根容器),isSVG作為參數(shù)傳入render函數(shù)中去進(jìn)行渲染。
虛擬節(jié)點(diǎn)其實(shí)就是JavaScript的一個(gè)對(duì)象,用來(lái)描述DOM。
這里可以編寫一個(gè)實(shí)際的簡(jiǎn)單例子來(lái)輔助理解,下面是一段html的普通元素節(jié)點(diǎn)
<div class="title" >這是一個(gè)標(biāo)題</div>
如何用虛擬節(jié)點(diǎn)來(lái)表示?
const VNode ={ type:'div', props:{ class:'title', style:{ fontSize:'16px', width:'100px' } }, children:'這是一個(gè)標(biāo)題', key:null }
這里官方文檔給出了建議:完整的 VNode
接口包含其他內(nèi)部屬性,但是強(qiáng)烈建議避免使用這些沒有在這里列舉出的屬性。這樣能夠避免因內(nèi)部屬性變更而導(dǎo)致的不兼容性問(wèn)題。
vue3對(duì)vnode的type做了更詳細(xì)的分類。在創(chuàng)建vnode之前先了解一下shapeFlags
,這個(gè)類對(duì)type的類型信息做了對(duì)應(yīng)的編碼。以便之后在patch階段,可以通過(guò)不同的類型執(zhí)行對(duì)應(yīng)的邏輯處理。同時(shí)也能看到type有元素,方法函數(shù)組件,帶狀態(tài)的組件,子類是文本等。
// package/shared/src/shapeFlags.ts //這是一個(gè)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 //傳送,將一個(gè)組件內(nèi)部的模板‘傳送'到該組件DOM結(jié)構(gòu)外層中去,例如遮罩層的使用 TELEPORT = 1 << 6,//64 //懸念,用于等待異步組件時(shí)渲染一些額外的內(nèi)容,比如骨架屏,不過(guò)目前是實(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
它用來(lái)表示當(dāng)前虛擬節(jié)點(diǎn)的類型。我們可以通過(guò)對(duì)shapeFlag
做二進(jìn)制運(yùn)算來(lái)描述當(dāng)前節(jié)點(diǎn)的本身是什么類型、子節(jié)點(diǎn)是什么類型。
因?yàn)関node可以抽象,把渲染的過(guò)程抽象化,使組件的抽象能力也得到提升。 然后因?yàn)関ue需要可以跨平臺(tái),講節(jié)點(diǎn)抽象化后可以通過(guò)平臺(tái)自己的實(shí)現(xiàn),使之在各個(gè)平臺(tái)上渲染更容易。 不過(guò)同時(shí)需要注意的一點(diǎn),雖然使用的是vnode,但是這并不意味著vnode的性能更具有優(yōu)勢(shì)。比如很大的組件,是表格上千行的表格,在render過(guò)程中,創(chuàng)建vnode勢(shì)必得遍歷上千次vnode的創(chuàng)建,然后遍歷上千次的patch,在更新表格數(shù)據(jù)中,勢(shì)必會(huì)出現(xiàn)卡頓的情況。即便是在patch中使用diff優(yōu)化了對(duì)DOM操作次數(shù),但是始終需要操作。
vue3 提供了一個(gè) 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, //動(dòng)態(tài)參數(shù) dynamicProps: string[] | null = null, //是否是block節(jié)點(diǎn) isBlockNode = false ): VNode { //內(nèi)部邏輯處理 //使用更基層的createBaseVNode對(duì)各項(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 確保在克隆過(guò)程中合并refs,而不是覆蓋它。 const cloned = cloneVNode(type, props, true /* mergeRef: true */) //如果擁有子節(jié)點(diǎn),將子節(jié)點(diǎn)規(guī)范化處理 if (children) {normalizeChildren(cloned, children)}: //將拷貝的對(duì)象存入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) { //對(duì)于響應(yīng)式或者代理的對(duì)象,我們需要克隆來(lái)處理,以防止觸發(fā)響應(yīng)式和代理的變動(dòng) props = guardReactiveProps(props)! let { class: klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) } if (isObject(style)) { // 響應(yīng)式對(duì)象需要克隆后再處理,以免觸發(fā)響應(yīng)式。 if (isProxy(style) && !isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } }
與之前的shapeFlags枚舉類結(jié)合,將定好的編碼賦值給shapeFlag
// 將虛擬節(jié)點(diǎn)的類型信息編碼成一個(gè)位圖(bitmap) // 根據(jù)type類型來(lái)確定shapeFlag的屬性值 const shapeFlag = isString(type)//是否是字符串 ? ShapeFlags.ELEMENT//傳值1 : __FEATURE_SUSPENSE__ && isSuspense(type)//是否是懸念類型 ? ShapeFlags.SUSPENSE//傳值128 : isTeleport(type)//是否是傳送類型 ? ShapeFlags.TELEPORT//傳值64 : isObject(type)//是否是對(duì)象類型 ? 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ì)的屬性對(duì)象創(chuàng)建。
創(chuàng)建基礎(chǔ)虛擬節(jié)點(diǎn)(JavaScript對(duì)象),初始化封裝一系列相關(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,//動(dòng)態(tài)參數(shù)內(nèi)容 shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,//節(jié)點(diǎn)類型的信息編碼 isBlockNode = false,//是否塊節(jié)點(diǎn) needFullChildrenNormalization = false ) { //聲明一個(gè)vnode對(duì)象,并且將各種屬性賦值,從而完成虛擬節(jié)點(diǎn)的初始化創(chuàng)建 const vnode = { __v_isVNode: true,//內(nèi)部屬性表示為Vnode __v_skip: true,//表示跳過(guò)響應(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元素的時(shí)候產(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,//動(dòng)態(tài)參數(shù) dynamicChildren: null,//動(dòng)態(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就是一個(gè)對(duì)props中的內(nèi)容進(jìn)行標(biāo)準(zhǔn)化處理,然后對(duì)節(jié)點(diǎn)類型進(jìn)行信息編碼,對(duì)子節(jié)點(diǎn)的標(biāo)準(zhǔn)化處理和類型信息編碼,最后創(chuàng)建vnode對(duì)象的過(guò)程。
baseCreateRenderer()
返回對(duì)象中,有render()
函數(shù),hydrate用于服務(wù)器渲染和createApp函數(shù)的。 在baseCreateRenderer()
函數(shù)中,定義了render()
函數(shù),render的內(nèi)容不復(fù)雜。
組件在首次掛載,以及后續(xù)的更新等,都會(huì)觸發(fā)mount()
,而這些,其實(shí)都會(huì)調(diào)用render()
渲染函數(shù)。render()
會(huì)先判斷vnode虛擬節(jié)點(diǎn)是否存在,如果不存在進(jìn)行unmount()
卸載操作。 如果存在則會(huì)調(diào)用patch()
函數(shù)。因此可以推測(cè),patch()
的過(guò)程中,有關(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ù)看,會(huì)傳入新舊節(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)賦值到容器上 }
這里來(lái)看一下有關(guān)patch()
函數(shù)的代碼,側(cè)重了解當(dāng)組件初次渲染的時(shí)候的流程。
// 注意:此閉包中的函數(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),用來(lái)標(biāo)識(shí)當(dāng)我們對(duì)新舊節(jié)點(diǎn)做增刪或移動(dòng)等操作時(shí),以哪個(gè)節(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ù)位 } //是否動(dòng)態(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)之間的對(duì)比,這也是常說(shuō)的diff算法,結(jié)合render(vnode, rootContainer, isSVG)
可以看出vnode對(duì)應(yīng)的是n1也就是新節(jié)點(diǎn),而rootContainer對(duì)應(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)移向下個(gè)節(jié)點(diǎn)。
新節(jié)點(diǎn)是否是動(dòng)態(tài)值優(yōu)化標(biāo)記
對(duì)新節(jié)點(diǎn)的類型判斷
文本類:processText
注釋類:processComment
靜態(tài)類:mountStaticNode
片段類:processFragment
默認(rèn)
而這個(gè)默認(rèn)才是主要的部分也是最常用到的部分。里面包含了對(duì)類型是元素element
、組件component
、傳送teleport
、懸念suspense
的處理。這次主要講的是虛擬節(jié)點(diǎn)到組件和普通元素渲染的過(guò)程,其他類型的暫時(shí)不提,內(nèi)容展開過(guò)于雜亂。
實(shí)際上第一次初始運(yùn)行的時(shí)候,patch判斷vnode類型根節(jié)點(diǎn),因?yàn)関ue3書寫的時(shí)候,都是以組件的形式體現(xiàn),所以第一次的類型勢(shì)必是component類型。
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)不存在,初次渲染的時(shí)候 //省略一部分n2其他情況下的處理 //掛載組件 mountComponent( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { //更新組件 updateComponent(n1, n2, optimized) } }
老節(jié)點(diǎn)n1不存在null
的時(shí)候,將掛載n2節(jié)點(diǎn)。如果老節(jié)點(diǎn)存在的時(shí)候,則更新組件。因此mountComponent()
最常見的就是在首次渲染的時(shí)候,那時(shí)舊節(jié)點(diǎn)都是空的。
接下來(lái)就是看如何掛載組件mountComponent()
const mountComponent: MountComponentFn = ( initialVNode,//對(duì)應(yīng)n2 新的節(jié)點(diǎn) container,//對(duì)應(yīng)宿主 anchor,//錨點(diǎn) parentComponent,//父組件 parentSuspense,//父?jìng)魉? 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()是異步的。這個(gè)組件在進(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,//存儲(chǔ)了新節(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)部以對(duì)象的方式創(chuàng)建渲染組件實(shí)例。 設(shè)置組件實(shí)例,是將組件中許多數(shù)據(jù),賦值給了instance,維護(hù)組件上下文,同時(shí)對(duì)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)建子樹,同時(shí) const subTree = (instance.subTree = renderComponentRoot(instance)) //遞歸 patch( null,//因?yàn)槭菕燧d,所以n1這個(gè)老節(jié)點(diǎn)是空的。 subTree,//子樹賦值到n2這個(gè)新節(jié)點(diǎn) container,//掛載到container上 anchor, instance, parentSuspense, isSVG ) //保留渲染生成的子樹DOM節(jié)點(diǎn) initialVNode.el = subTree.el // 已掛載鉤子 // 掛在后的節(jié)點(diǎn) //激活為了緩存根的鉤子 // #1742 激活的鉤子必須在第一次渲染后被訪問(wèn) 因?yàn)樵撱^子可能會(huì)被子類的keep-alive注入。 instance.isMounted = true // #2458: deference mount-only object parameters to prevent memleaks // #2458: 遵從只掛載對(duì)象的參數(shù)以防止內(nèi)存泄漏 initialVNode = container = anchor = null as any } else { // 更新組件 // 這是由組件自身狀態(tài)的突變觸發(fā)的(next: null)?;蛘吒讣?jí)調(diào)用processComponent(下一個(gè):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)式模塊,該模塊的主要功能就是,讓對(duì)象屬性具有響應(yīng)式功能,當(dāng)其中的屬性發(fā)生了變動(dòng),那effect副作用所包含的函數(shù)也會(huì)重新執(zhí)行一遍,從而讓界面重新渲染。這一塊內(nèi)容先不管。從effect函數(shù)看,明白了調(diào)用了componentUpdateFn
, 即組件更新方法,這個(gè)方法涉及了2個(gè)條件,一個(gè)是初次運(yùn)行的掛載,而另一個(gè)是節(jié)點(diǎn)變動(dòng)后的更新組件。 componentUpdateFn
中進(jìn)行的初次渲染,主要是生成了subTree
然后把subTree
傳遞到patch進(jìn)行了遞歸掛載到container上。
subTree也是一個(gè)vnode對(duì)象,然而這里的subTree和initialVNode是不同的。以下面舉個(gè)例子:
<template> <div class="app"> <p>title</p> <helloWorld> </div> </template>
而helloWorld組件中是<div>標(biāo)簽包含一個(gè)<p>標(biāo)簽
<template> <div class="hello"> <p>hello world</p> </div> </template>
在App組件中,<helloWorld> 節(jié)點(diǎn)渲染渲染生成的vnode就是 helloWorld組件的initialVNode,而這個(gè)組件內(nèi)部所有的DOM節(jié)點(diǎn)就是vnode通過(guò)執(zhí)行renderComponentRoot
渲染生成的的subTree。 每個(gè)組件渲染的時(shí)候都會(huì)運(yùn)行render函數(shù),renderComponentRoot
就是去執(zhí)行render函數(shù)創(chuàng)建整個(gè)組件內(nèi)部的vnode,然后進(jìn)行標(biāo)準(zhǔn)化就得到了該函數(shù)的返回結(jié)果:子樹vnode。 生成子樹后,接下來(lái)就是繼續(xù)調(diào)用patch函數(shù)把子樹vnode掛載到container上去。 回到patch后,就會(huì)繼續(xù)對(duì)子樹vnode進(jìn)行判斷,例如上面的App組件的根節(jié)點(diǎn)是<div>標(biāo)簽,而對(duì)應(yīng)的subTree就是普通元素vnode,接下來(lái)就是堆普通Element處理的流程。
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的時(shí)候,運(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 對(duì)于內(nèi)部懸念+懸念未解決的情況,進(jìn)入鉤子應(yīng)該在懸念解決時(shí)調(diào)用。 // #1689 對(duì)于內(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),同時(shí)對(duì)props相關(guān)處理。
所以根據(jù)代碼,首先是通過(guò)hostCreateElement方法創(chuàng)建了DOM元素節(jié)點(diǎn)。
const {createElement:hostCreateElement } = options
是從options這個(gè)實(shí)參中解構(gòu)并重命名為hostCreateElement
方法的,那么這個(gè)實(shí)參是從哪里來(lái) 需要追溯一下,回到初次渲染開始的流程中去。
從這流程圖可以清楚的知道,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)建元素。
說(shuō)回上面,創(chuàng)建完DOM節(jié)點(diǎn)元素之后,接下來(lái)是繼續(xù)判斷子節(jié)點(diǎn)的類型,如果子節(jié)點(diǎn)是文本類型的,則調(diào)用處理文本hostSetElementText()
方法。
const {setElementText: hostSetElementText} = option setElementText: (el, text) => { el.textContent = text },
與前面的createElement一樣,setElementText方法是通過(guò)設(shè)置DOM元素的textContent屬性設(shè)置文本。
而如果子節(jié)點(diǎn)的類型是數(shù)組類,則執(zhí)行mountChildren方法,對(duì)子節(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)的掛載邏輯看起來(lái)會(huì)非常眼熟,在對(duì)children數(shù)組進(jìn)行遍歷之后獲取到的每一個(gè)child,進(jìn)行預(yù)處理后并對(duì)其執(zhí)行掛載方法。 結(jié)合之前調(diào)用mountChildren()
方法傳入的實(shí)參和其形參之間的對(duì)比。
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 )
明確的對(duì)應(yīng)上了第二個(gè)參數(shù)是container,而調(diào)用mountChildren
方法時(shí)傳入第二個(gè)參數(shù)的是在調(diào)用mountElement()
時(shí)創(chuàng)建的DOM節(jié)點(diǎn),這樣便建立起了父子關(guān)系。 而且,后續(xù)的繼續(xù)遞歸patch()
,能深度遍歷樹的方式,可以完整的把DOM樹遍歷出來(lái),完成渲染。
處理完節(jié)點(diǎn)的后,最后會(huì)調(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_雖然是可選的對(duì)象,但是實(shí)際上,在不同的瀏覽器會(huì)有不同的表現(xiàn)形式,所以如果沒有existingnode值的情況下,填入null會(huì)將新的節(jié)點(diǎn)添加到node子節(jié)點(diǎn)的尾部。
關(guān)于“Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。