您好,登錄后才能下訂單哦!
這篇文章主要介紹了Vue3怎么將虛擬節點渲染到網頁初次渲染的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Vue3怎么將虛擬節點渲染到網頁初次渲染文章都會有所收獲,下面我們一起來看看吧。
createApp函數內部的app.mount方法是一個標準的可跨平臺的組件渲染流程:先創建VNode,再渲染VNode。
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 //生成一個具體的對象,提供對應的API和相關屬性 const app: App = (context.app = {//將以下參數傳入到context中的app里 //...省略其他邏輯處理 //掛載 mount( rootContainer: HostElement, isHydrate?: boolean,//是用來判斷是否用于服務器渲染,這里不講所以省略 isSVG?: boolean ): any { //如果處于未掛載完畢狀態下運行 if (!isMounted) { //創建一個新的虛擬節點傳入根組件和根屬性 const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // 存儲app上下文到根虛擬節點,這將在初始掛載時設置在根實例上。 vnode.appContext = context } //渲染虛擬節點,根容器 render(vnode, rootContainer, isSVG) isMounted = true //將狀態改變成為已掛載 app._container = rootContainer // for devtools and telemetry ;(rootContainer as any).__vue_app__ = app return getExposeProxy(vnode.component!) || vnode.component!.proxy }}, }) return app } }
在mount的過程中,當運行處于未掛載時, const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)創建虛擬節點并且將 vnode(虛擬節點)、rootContainer(根容器),isSVG作為參數傳入render函數中去進行渲染。
虛擬節點其實就是JavaScript的一個對象,用來描述DOM。
這里可以編寫一個實際的簡單例子來輔助理解,下面是一段html的普通元素節點
<div class="title" >這是一個標題</div>
如何用虛擬節點來表示?
const VNode ={ type:'div', props:{ class:'title', style:{ fontSize:'16px', width:'100px' } }, children:'這是一個標題', key:null }
這里官方文檔給出了建議:完整的 VNode
接口包含其他內部屬性,但是強烈建議避免使用這些沒有在這里列舉出的屬性。這樣能夠避免因內部屬性變更而導致的不兼容性問題。
vue3對vnode的type做了更詳細的分類。在創建vnode之前先了解一下shapeFlags
,這個類對type的類型信息做了對應的編碼。以便之后在patch階段,可以通過不同的類型執行對應的邏輯處理。同時也能看到type有元素,方法函數組件,帶狀態的組件,子類是文本等。
// package/shared/src/shapeFlags.ts //這是一個ts的枚舉類,從中也能了解到虛擬節點的類型 export const enum ShapeFlags { //DOM元素 HTML ELEMENT = 1, //函數式組件 FUNCTIONAL_COMPONENT = 1 << 1, //2 //帶狀態的組件 STATEFUL_COMPONENT = 1 << 2,//4 //子節點是文本 TEXT_CHILDREN = 1 << 3,//8 //子節點是數組 ARRAY_CHILDREN = 1 << 4,//16 //子節點帶有插槽 SLOTS_CHILDREN = 1 << 5,//32 //傳送,將一個組件內部的模板‘傳送'到該組件DOM結構外層中去,例如遮罩層的使用 TELEPORT = 1 << 6,//64 //懸念,用于等待異步組件時渲染一些額外的內容,比如骨架屏,不過目前是實驗性功能 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
它用來表示當前虛擬節點的類型。我們可以通過對shapeFlag
做二進制運算來描述當前節點的本身是什么類型、子節點是什么類型。
因為vnode可以抽象,把渲染的過程抽象化,使組件的抽象能力也得到提升。 然后因為vue需要可以跨平臺,講節點抽象化后可以通過平臺自己的實現,使之在各個平臺上渲染更容易。 不過同時需要注意的一點,雖然使用的是vnode,但是這并不意味著vnode的性能更具有優勢。比如很大的組件,是表格上千行的表格,在render過程中,創建vnode勢必得遍歷上千次vnode的創建,然后遍歷上千次的patch,在更新表格數據中,勢必會出現卡頓的情況。即便是在patch中使用diff優化了對DOM操作次數,但是始終需要操作。
vue3 提供了一個 h()
函數用于創建 vnodes:
import {h} from 'vue' h('div', { id: 'foo' })
其本質也是調用 createVNode()
函數。
const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)
createVNode()
位于 core/packages/runtime-core/src/vnode.ts
//創建虛擬節點 export const createVNode = ( _createVNode) as typeof _createVNode function _createVNode( //標簽類型 type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, //數據和vnode的屬性 props: (Data & VNodeProps) | null = null, //子節點 children: unknown = null, //patch標記 patchFlag: number = 0, //動態參數 dynamicProps: string[] | null = null, //是否是block節點 isBlockNode = false ): VNode { //內部邏輯處理 //使用更基層的createBaseVNode對各項參數進行處理 return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ) }
剛才省略的內部邏輯處理,這里去除了只有在開發環境下才運行的代碼:
if (isVNode(type)) { //創建虛擬節點接收到已存在的節點,這種情況發生在諸如 <component :is="vnode"/> // #2078 確保在克隆過程中合并refs,而不是覆蓋它。 const cloned = cloneVNode(type, props, true /* mergeRef: true */) //如果擁有子節點,將子節點規范化處理 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 }
// 類組件規范化 if (isClassComponent(type)) { type = type.__vccOpts } // 類(class)和風格(style) 規范化. if (props) { //對于響應式或者代理的對象,我們需要克隆來處理,以防止觸發響應式和代理的變動 props = guardReactiveProps(props)! let { class: klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) } if (isObject(style)) { // 響應式對象需要克隆后再處理,以免觸發響應式。 if (isProxy(style) && !isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } }
與之前的shapeFlags枚舉類結合,將定好的編碼賦值給shapeFlag
// 將虛擬節點的類型信息編碼成一個位圖(bitmap) // 根據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
以上,將虛擬節點其中一部分的屬性處理好之后,再傳入創建基礎虛擬節點函數中,做更進一步和更詳細的屬性對象創建。
創建基礎虛擬節點(JavaScript對象),初始化封裝一系列相關的屬性。
function createBaseVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,//虛擬節點類型 props: (Data & VNodeProps) | null = null,//內部的屬性 children: unknown = null,//子節點內容 patchFlag = 0,//patch標記 dynamicProps: string[] | null = null,//動態參數內容 shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,//節點類型的信息編碼 isBlockNode = false,//是否塊節點 needFullChildrenNormalization = false ) { //聲明一個vnode對象,并且將各種屬性賦值,從而完成虛擬節點的初始化創建 const vnode = { __v_isVNode: true,//內部屬性表示為Vnode __v_skip: true,//表示跳過響應式轉換 type, //虛擬節點類型 props,//虛擬節點內的屬性和props key: props && normalizeKey(props),//虛擬階段的key用于diff ref: props && normalizeRef(props),//引用 scopeId: currentScopeId,//作用域id slotScopeIds: null,//插槽id children,//子節點內容,樹形結構 component: null,//組件 suspense: null,//傳送組件 ssContent: null, ssFallback: null, dirs: null,//目錄 transition: null,//內置組件相關字段 el: null,//vnode實際被轉換為dom元素的時候產生的元素,宿主 anchor: null,//錨點 target: null,//目標 targetAnchor: null,//目標錨點 staticCount: 0,//靜態節點數 shapeFlag,//shape標記 patchFlag,//patch標記 dynamicProps,//動態參數 dynamicChildren: null,//動態子節點 appContext: null,//app上下文 ctx: currentRenderingInstance } as VNode //關于子節點和block節點的標準化和信息編碼處理 return vnode }
由此可見,創建vnode就是一個對props中的內容進行標準化處理,然后對節點類型進行信息編碼,對子節點的標準化處理和類型信息編碼,最后創建vnode對象的過程。
baseCreateRenderer()
返回對象中,有render()
函數,hydrate用于服務器渲染和createApp函數的。 在baseCreateRenderer()
函數中,定義了render()
函數,render的內容不復雜。
組件在首次掛載,以及后續的更新等,都會觸發mount()
,而這些,其實都會調用render()
渲染函數。render()
會先判斷vnode虛擬節點是否存在,如果不存在進行unmount()
卸載操作。 如果存在則會調用patch()
函數。因此可以推測,patch()
的過程中,有關組件相關處理。
const render: RootRenderFunction = (vnode, container, isSVG) => { if (vnode == null) {//判斷是否傳入虛擬節點,如果節點不存在則運行 if (container._vnode) {//判斷容器中是否已有節點 unmount(container._vnode, null, null, true)//如果已有節點則卸載當前節點 } } else { //如果節點存在,則調用patch函數,從參數看,會傳入新舊節點和容器 patch(container._vnode || null, vnode, container, null, null, null, isSVG) } flushPreFlushCbs() //組件更新前的回調 flushPostFlushCbs()//組件更新后的回調 container._vnode = vnode//將虛擬節點賦值到容器上 }
這里來看一下有關patch()
函數的代碼,側重了解當組件初次渲染的時候的流程。
// 注意:此閉包中的函數應使用 'const xxx = () => {}'樣式,以防止被小寫器內聯。 // patch:進行diff算法,crateApp->vnode->element const patch: PatchFn = ( n1,//老節點 n2,//新節點 container,//宿主元素 container anchor = null,//錨點,用來標識當我們對新舊節點做增刪或移動等操作時,以哪個節點為參照物 parentComponent = null,//父組件 parentSuspense = null,//父懸念 isSVG = false, slotScopeIds = null,//插槽 optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren ) => { if (n1 === n2) {// 如果新老節點相同則停止 return } // 打補丁且不是相同類型,則卸載舊節點,錨點后移 if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null //n1復位 } //是否動態節點優化 if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } //結構n2新節點,獲取新節點的類型 const { type, ref, shapeFlag } = n2 switch (type) { case Text: //文本類 processText(n1, n2, container, anchor)//文本節點處理 break case Comment://注釋類 processCommentNode(n1, n2, container, anchor)//處理注釋節點 break case Static://靜態類 if (n1 == null) {//如果老節點不存在 mountStaticNode(n2, container, anchor, isSVG)//掛載靜態節點 } break case Fragment://片段類 processFragment( //進行片段處理 ) 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( // 如果類型是傳送,進行處理 ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process( //懸念處理 ) } } // 設置 參考 ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } }
patch函數可見,主要做的就是 新舊虛擬節點之間的對比,這也是常說的diff算法,結合render(vnode, rootContainer, isSVG)
可以看出vnode對應的是n1也就是新節點,而rootContainer對應n2,也就是老節點。其做的邏輯判斷是。
新舊節點相同則直接返回
舊節點存在,且新節點和舊節點的類型不同,舊節點將被卸載unmount
且復位清空null
。錨點移向下個節點。
新節點是否是動態值優化標記
對新節點的類型判斷
文本類:processText
注釋類:processComment
靜態類:mountStaticNode
片段類:processFragment
默認
而這個默認才是主要的部分也是最常用到的部分。里面包含了對類型是元素element
、組件component
、傳送teleport
、懸念suspense
的處理。這次主要講的是虛擬節點到組件和普通元素渲染的過程,其他類型的暫時不提,內容展開過于雜亂。
實際上第一次初始運行的時候,patch判斷vnode類型根節點,因為vue3書寫的時候,都是以組件的形式體現,所以第一次的類型勢必是component類型。
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) {//如果老節點不存在,初次渲染的時候 //省略一部分n2其他情況下的處理 //掛載組件 mountComponent( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { //更新組件 updateComponent(n1, n2, optimized) } }
老節點n1不存在null
的時候,將掛載n2節點。如果老節點存在的時候,則更新組件。因此mountComponent()
最常見的就是在首次渲染的時候,那時舊節點都是空的。
接下來就是看如何掛載組件mountComponent()
const mountComponent: MountComponentFn = ( initialVNode,//對應n2 新的節點 container,//對應宿主 anchor,//錨點 parentComponent,//父組件 parentSuspense,//父傳送 isSVG,//是否SVG optimized//是否優化 ) => { // 2.x編譯器可以在實際安裝前預先創建組件實例。 const compatMountInstance = //判斷是不是根組件且是組件 __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component const instance: ComponentInternalInstance = compatMountInstance || //創建組件實例 (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )) // 如果新節點是緩存組件的話那么將internals賦值給期渲染函數 if (isKeepAlive(initialVNode)) { ;(instance.ctx as KeepAliveContext).renderer = internals } // 為了設置上下文處理props和slot插槽 if (!(__COMPAT__ && compatMountInstance)) { //設置組件實例 setupComponent(instance) } //setup()是異步的。這個組件在進行之前依賴于異步邏輯的解決 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 } //設置運行渲染副作用函數 setupRenderEffect( instance,//存儲了新節點的組件上下文,props插槽等其他實例屬性 initialVNode,//新節點n2 container,//容器 anchor,//錨點 parentSuspense,//父懸念 isSVG,//是否SVG optimized//是否優化 ) }
掛載組件中,除開緩存和懸掛上的函數處理,其邏輯上基本為:創建組件的實例createComponentInstance()
,設置組件實例 setupComponent(instance)
和設置運行渲染副作用函數setupRenderEffect()
。
創建組件實例,基本跟創建虛擬節點一樣的,內部以對象的方式創建渲染組件實例。 設置組件實例,是將組件中許多數據,賦值給了instance,維護組件上下文,同時對props和插槽等屬性初始化處理。
然后是setupRenderEffect
設置渲染副作用函數;
const setupRenderEffect: SetupRenderEffectFn = ( instance,//實例 initialVNode,//初始化節點 container,//容器 anchor,//錨點 parentSuspense,//父懸念 isSVG,//是否是SVG optimized//優化標記 ) => { //組件更新方法 const componentUpdateFn = () => { //如果組件處于未掛載的狀態下 if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined //解構 const { el, props } = initialVNode const { bm, m, parent } = instance const isAsyncWrapperVNode = isAsyncWrapper(initialVNode) toggleRecurse(instance, false) // 掛載前的鉤子 // 掛載前的節點 toggleRecurse(instance, true) //這部分是跟服務器渲染相關的邏輯處理 //創建子樹,同時 const subTree = (instance.subTree = renderComponentRoot(instance)) //遞歸 patch( null,//因為是掛載,所以n1這個老節點是空的。 subTree,//子樹賦值到n2這個新節點 container,//掛載到container上 anchor, instance, parentSuspense, isSVG ) //保留渲染生成的子樹DOM節點 initialVNode.el = subTree.el // 已掛載鉤子 // 掛在后的節點 //激活為了緩存根的鉤子 // #1742 激活的鉤子必須在第一次渲染后被訪問 因為該鉤子可能會被子類的keep-alive注入。 instance.isMounted = true // #2458: deference mount-only object parameters to prevent memleaks // #2458: 遵從只掛載對象的參數以防止內存泄漏 initialVNode = container = anchor = null as any } else { // 更新組件 // 這是由組件自身狀態的突變觸發的(next: null)。或者父級調用processComponent(下一個:VNode)。 } } // 創建用于渲染的響應式副作用 const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, () => queueJob(update), instance.scope // 在組件的效果范圍內跟蹤它 )) //更新方法 const update: SchedulerJob = (instance.update = () => effect.run()) //實例的uid賦值給更新的id update.id = instance.uid // 允許遞歸 // #1801, #2043 組件渲染效果應允許遞歸更新 toggleRecurse(instance, true) update() }
setupRenderEffect()
最后執行的了 update()
方法,其實是運行了effect.run()
,并且將其賦值給了instance.updata中。而 effect 涉及到了 vue3 的響應式模塊,該模塊的主要功能就是,讓對象屬性具有響應式功能,當其中的屬性發生了變動,那effect副作用所包含的函數也會重新執行一遍,從而讓界面重新渲染。這一塊內容先不管。從effect函數看,明白了調用了componentUpdateFn
, 即組件更新方法,這個方法涉及了2個條件,一個是初次運行的掛載,而另一個是節點變動后的更新組件。 componentUpdateFn
中進行的初次渲染,主要是生成了subTree
然后把subTree
傳遞到patch進行了遞歸掛載到container上。
subTree也是一個vnode對象,然而這里的subTree和initialVNode是不同的。以下面舉個例子:
<template> <div class="app"> <p>title</p> <helloWorld> </div> </template>
而helloWorld組件中是<div>標簽包含一個<p>標簽
<template> <div class="hello"> <p>hello world</p> </div> </template>
在App組件中,<helloWorld> 節點渲染渲染生成的vnode就是 helloWorld組件的initialVNode,而這個組件內部所有的DOM節點就是vnode通過執行renderComponentRoot
渲染生成的的subTree。 每個組件渲染的時候都會運行render函數,renderComponentRoot
就是去執行render函數創建整個組件內部的vnode,然后進行標準化就得到了該函數的返回結果:子樹vnode。 生成子樹后,接下來就是繼續調用patch函數把子樹vnode掛載到container上去。 回到patch后,就會繼續對子樹vnode進行判斷,例如上面的App組件的根節點是<div>標簽,而對應的subTree就是普通元素vnode,接下來就是堆普通Element處理的流程。
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 mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { //如果是更新節點則運行patchElement patchElement( n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }
邏輯依舊,如果有n1老節點為null的時候,運行掛載元素的邏輯,否則運行更新元素節點的方法。
以下是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 //創建元素節點 el = vnode.el = hostCreateElement( vnode.type as string, isSVG, props && props.is, props ) // 首先掛載子類,因為某些props依賴于子類內容 // 已經渲染, 例如 `<select value>` // 如果標記判斷子節點類型是文本類型 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 處理子節點是純文本的情況 hostSetElementText(el, vnode.children as string) //如果標記類型是數組子類 } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { //掛載子類,進行patch后進行掛載 mountChildren( vnode.children as VNodeArrayChildren, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized ) } if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'created') } // 設置范圍id setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent) // props相關的處理,比如 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 對于內部懸念+懸念未解決的情況,進入鉤子應該在懸念解決時調用。 // #1689 對于內部懸念+懸念解決的情況,只需調用它 const needCallTransitionHooks = (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) && transition && !transition.persisted if (needCallTransitionHooks) { transition!.beforeEnter(el) } //把創建的元素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
掛載元素主要做了,創建DOM元素節點,處理節點子節點,掛載子節點,同時對props相關處理。
所以根據代碼,首先是通過hostCreateElement方法創建了DOM元素節點。
const {createElement:hostCreateElement } = options
是從options這個實參中解構并重命名為hostCreateElement
方法的,那么這個實參是從哪里來 需要追溯一下,回到初次渲染開始的流程中去。
從這流程圖可以清楚的知道,options
中createElement
方法是從nodeOps.ts
文件中導出的并傳入baseCreateRender()
方法內的。
該文件位于: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 },
從中可以看出,其實是調用了底層的DOM API document.createElement創建元素。
說回上面,創建完DOM節點元素之后,接下來是繼續判斷子節點的類型,如果子節點是文本類型的,則調用處理文本hostSetElementText()
方法。
const {setElementText: hostSetElementText} = option setElementText: (el, text) => { el.textContent = text },
與前面的createElement一樣,setElementText方法是通過設置DOM元素的textContent屬性設置文本。
而如果子節點的類型是數組類,則執行mountChildren方法,對子節點進行掛載:
const mountChildren: MountChildrenFn = ( children,//子節點數組里的內容 container,//容器 anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized,//優化標記 start = 0 ) => { //遍歷子節點中的內容 for (let i = start; i < children.length; i++) { //根據優化標記進行判斷進行克隆或者節點初始化處理。 const child = (children[i] = optimized ? cloneIfMounted(children[i] as VNode) : normalizeVNode(children[i])) //執行patch方法,遞歸掛載child patch( null,//因為是初次掛載所以沒有老的節點 child,//虛擬子節點 container,//容器 anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }
子節點的掛載邏輯看起來會非常眼熟,在對children數組進行遍歷之后獲取到的每一個child,進行預處理后并對其執行掛載方法。 結合之前調用mountChildren()
方法傳入的實參和其形參之間的對比。
mountChildren( vnode.children as VNodeArrayChildren, //節點中子節點的內容 el,//DOM元素 null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized ) const mountChildren: MountChildrenFn = ( children,//子節點數組里的內容 container,//容器 anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized,//優化標記 start = 0 )
明確的對應上了第二個參數是container,而調用mountChildren
方法時傳入第二個參數的是在調用mountElement()
時創建的DOM節點,這樣便建立起了父子關系。 而且,后續的繼續遞歸patch()
,能深度遍歷樹的方式,可以完整的把DOM樹遍歷出來,完成渲染。
處理完節點的后,最后會調用 hostInsert(el, container, anchor)
const {insert: hostInsert} = option insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null) },
再次就用調用DOM方法將子類的內容掛載到parent,也就是把child掛載到parent下,完成節點的掛載。
注意點:node.insertBefore(newnode,existingnode)中_existingnode_雖然是可選的對象,但是實際上,在不同的瀏覽器會有不同的表現形式,所以如果沒有existingnode值的情況下,填入null會將新的節點添加到node子節點的尾部。
關于“Vue3怎么將虛擬節點渲染到網頁初次渲染”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Vue3怎么將虛擬節點渲染到網頁初次渲染”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。