Vue3將虛擬節(jié)點渲染到網(wǎng)頁初次渲染詳解
正文
在上一篇中,只講了大致的執(zhí)行流程,其中有關(guān)渲染部分的內(nèi)容并沒有深入,而這部分關(guān)系到創(chuàng)建Vnode和渲染Vnode的過程,就是將代碼通過渲染變成大家可見的網(wǎng)頁畫面的這一部分內(nèi)容。 createApp函數(shù)內(nèi)部的app.mount方法是一個標準的可跨平臺的組件渲染流程:先創(chuàng)建VNode,再渲染VNode。
何時會進行虛擬函數(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 //生成一個具體的對象,提供對應的API和相關(guān)屬性 const app: App = (context.app = {//將以下參數(shù)傳入到context中的app里 //...省略其他邏輯處理 //掛載 mount( rootContainer: HostElement, isHydrate?: boolean,//是用來判斷是否用于服務器渲染,這里不講所以省略 isSVG?: boolean ): any { //如果處于未掛載完畢狀態(tài)下運行 if (!isMounted) { //創(chuàng)建一個新的虛擬節(jié)點傳入根組件和根屬性 const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // 存儲app上下文到根虛擬節(jié)點,這將在初始掛載時設(shè)置在根實例上。 vnode.appContext = context } //渲染虛擬節(jié)點,根容器 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的過程中,當運行處于未掛載時, const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)創(chuàng)建虛擬節(jié)點并且將 vnode(虛擬節(jié)點)、rootContainer(根容器),isSVG作為參數(shù)傳入render函數(shù)中去進行渲染。
什么是VNode?
虛擬節(jié)點其實就是JavaScript的一個對象,用來描述DOM。
這里可以編寫一個實際的簡單例子來輔助理解,下面是一段html的普通元素節(jié)點
<div class="title" style="font-size:16px;width=100px">這是一個標題</div>
如何用虛擬節(jié)點來表示?
const VNode ={ type:'div', props:{ class:'title', style:{ fontSize:'16px', width:'100px' } }, children:'這是一個標題', key:null }
這里官方文檔給出了建議:完整的 VNode
接口包含其他內(nèi)部屬性,但是強烈建議避免使用這些沒有在這里列舉出的屬性。這樣能夠避免因內(nèi)部屬性變更而導致的不兼容性問題。
vue3對vnode的type做了更詳細的分類。在創(chuàng)建vnode之前先了解一下shapeFlags
,這個類對type的類型信息做了對應的編碼。以便之后在patch階段,可以通過不同的類型執(zhí)行對應的邏輯處理。同時也能看到type有元素,方法函數(shù)組件,帶狀態(tài)的組件,子類是文本等。
前置須知
ShapeFlags
// package/shared/src/shapeFlags.ts //這是一個ts的枚舉類,從中也能了解到虛擬節(jié)點的類型 export const enum ShapeFlags { //DOM元素 HTML ELEMENT = 1, //函數(shù)式組件 FUNCTIONAL_COMPONENT = 1 << 1, //2 //帶狀態(tài)的組件 STATEFUL_COMPONENT = 1 << 2,//4 //子節(jié)點是文本 TEXT_CHILDREN = 1 << 3,//8 //子節(jié)點是數(shù)組 ARRAY_CHILDREN = 1 << 4,//16 //子節(jié)點帶有插槽 SLOTS_CHILDREN = 1 << 5,//32 //傳送,將一個組件內(nèi)部的模板‘傳送'到該組件DOM結(jié)構(gòu)外層中去,例如遮罩層的使用 TELEPORT = 1 << 6,//64 //懸念,用于等待異步組件時渲染一些額外的內(nèi)容,比如骨架屏,不過目前是實驗性功能 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
它用來表示當前虛擬節(jié)點的類型。我們可以通過對shapeFlag
做二進制運算來描述當前節(jié)點的本身是什么類型、子節(jié)點是什么類型。
為什么要使用Vnode?
因為vnode可以抽象,把渲染的過程抽象化,使組件的抽象能力也得到提升。 然后因為vue需要可以跨平臺,講節(jié)點抽象化后可以通過平臺自己的實現(xià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é)點 export const createVNode = ( _createVNode) as typeof _createVNode function _createVNode( //標簽類型 type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, //數(shù)據(jù)和vnode的屬性 props: (Data & VNodeProps) | null = null, //子節(jié)點 children: unknown = null, //patch標記 patchFlag: number = 0, //動態(tài)參數(shù) dynamicProps: string[] | null = null, //是否是block節(jié)點 isBlockNode = false ): VNode { //內(nèi)部邏輯處理 //使用更基層的createBaseVNode對各項參數(shù)進行處理 return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ) }
剛才省略的內(nèi)部邏輯處理,這里去除了只有在開發(fā)環(huán)境下才運行的代碼:
先是判斷
if (isVNode(type)) { //創(chuàng)建虛擬節(jié)點接收到已存在的節(jié)點,這種情況發(fā)生在諸如 <component :is="vnode"/> // #2078 確保在克隆過程中合并refs,而不是覆蓋它。 const cloned = cloneVNode(type, props, true /* mergeRef: true */) //如果擁有子節(jié)點,將子節(jié)點規(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)和風格(style) 規(guī)范化. if (props) { //對于響應式或者代理的對象,我們需要克隆來處理,以防止觸發(fā)響應式和代理的變動 props = guardReactiveProps(props)! let { class: klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) } if (isObject(style)) { // 響應式對象需要克隆后再處理,以免觸發(fā)響應式。 if (isProxy(style) && !isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } }
與之前的shapeFlags枚舉類結(jié)合,將定好的編碼賦值給shapeFlag
// 將虛擬節(jié)點的類型信息編碼成一個位圖(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é)點其中一部分的屬性處理好之后,再傳入創(chuàng)建基礎(chǔ)虛擬節(jié)點函數(shù)中,做更進一步和更詳細的屬性對象創(chuàng)建。
createBaseVNode 虛擬節(jié)點初始化創(chuàng)建
創(chuàng)建基礎(chǔ)虛擬節(jié)點(JavaScript對象),初始化封裝一系列相關(guān)的屬性。
function createBaseVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,//虛擬節(jié)點類型 props: (Data & VNodeProps) | null = null,//內(nèi)部的屬性 children: unknown = null,//子節(jié)點內(nèi)容 patchFlag = 0,//patch標記 dynamicProps: string[] | null = null,//動態(tài)參數(shù)內(nèi)容 shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,//節(jié)點類型的信息編碼 isBlockNode = false,//是否塊節(jié)點 needFullChildrenNormalization = false ) { //聲明一個vnode對象,并且將各種屬性賦值,從而完成虛擬節(jié)點的初始化創(chuàng)建 const vnode = { __v_isVNode: true,//內(nèi)部屬性表示為Vnode __v_skip: true,//表示跳過響應式轉(zhuǎn)換 type, //虛擬節(jié)點類型 props,//虛擬節(jié)點內(nèi)的屬性和props key: props && normalizeKey(props),//虛擬階段的key用于diff ref: props && normalizeRef(props),//引用 scopeId: currentScopeId,//作用域id slotScopeIds: null,//插槽id children,//子節(jié)點內(nèi)容,樹形結(jié)構(gòu) component: null,//組件 suspense: null,//傳送組件 ssContent: null, ssFallback: null, dirs: null,//目錄 transition: null,//內(nèi)置組件相關(guān)字段 el: null,//vnode實際被轉(zhuǎn)換為dom元素的時候產(chǎn)生的元素,宿主 anchor: null,//錨點 target: null,//目標 targetAnchor: null,//目標錨點 staticCount: 0,//靜態(tài)節(jié)點數(shù) shapeFlag,//shape標記 patchFlag,//patch標記 dynamicProps,//動態(tài)參數(shù) dynamicChildren: null,//動態(tài)子節(jié)點 appContext: null,//app上下文 ctx: currentRenderingInstance } as VNode //關(guān)于子節(jié)點和block節(jié)點的標準化和信息編碼處理 return vnode }
由此可見,創(chuàng)建vnode就是一個對props中的內(nèi)容進行標準化處理,然后對節(jié)點類型進行信息編碼,對子節(jié)點的標準化處理和類型信息編碼,最后創(chuàng)建vnode對象的過程。
render 渲染 VNode
baseCreateRenderer()
返回對象中,有render()
函數(shù),hydrate用于服務器渲染和createApp函數(shù)的。 在baseCreateRenderer()
函數(shù)中,定義了render()
函數(shù),render的內(nèi)容不復雜。
組件在首次掛載,以及后續(xù)的更新等,都會觸發(fā)mount()
,而這些,其實都會調(diào)用render()
渲染函數(shù)。render()
會先判斷vnode虛擬節(jié)點是否存在,如果不存在進行unmount()
卸載操作。 如果存在則會調(diào)用patch()
函數(shù)。因此可以推測,patch()
的過程中,有關(guān)組件相關(guān)處理。
const render: RootRenderFunction = (vnode, container, isSVG) => { if (vnode == null) {//判斷是否傳入虛擬節(jié)點,如果節(jié)點不存在則運行 if (container._vnode) {//判斷容器中是否已有節(jié)點 unmount(container._vnode, null, null, true)//如果已有節(jié)點則卸載當前節(jié)點 } } else { //如果節(jié)點存在,則調(diào)用patch函數(shù),從參數(shù)看,會傳入新舊節(jié)點和容器 patch(container._vnode || null, vnode, container, null, null, null, isSVG) } flushPreFlushCbs() //組件更新前的回調(diào) flushPostFlushCbs()//組件更新后的回調(diào) container._vnode = vnode//將虛擬節(jié)點賦值到容器上 }
patch VNode
這里來看一下有關(guān)patch()
函數(shù)的代碼,側(cè)重了解當組件初次渲染的時候的流程。
// 注意:此閉包中的函數(shù)應使用 'const xxx = () => {}'樣式,以防止被小寫器內(nèi)聯(lián)。 // patch:進行diff算法,crateApp->vnode->element const patch: PatchFn = ( n1,//老節(jié)點 n2,//新節(jié)點 container,//宿主元素 container anchor = null,//錨點,用來標識當我們對新舊節(jié)點做增刪或移動等操作時,以哪個節(jié)點為參照物 parentComponent = null,//父組件 parentSuspense = null,//父懸念 isSVG = false, slotScopeIds = null,//插槽 optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren ) => { if (n1 === n2) {// 如果新老節(jié)點相同則停止 return } // 打補丁且不是相同類型,則卸載舊節(jié)點,錨點后移 if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null //n1復位 } //是否動態(tài)節(jié)點優(yōu)化 if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } //結(jié)構(gòu)n2新節(jié)點,獲取新節(jié)點的類型 const { type, ref, shapeFlag } = n2 switch (type) { case Text: //文本類 processText(n1, n2, container, anchor)//文本節(jié)點處理 break case Comment://注釋類 processCommentNode(n1, n2, container, anchor)//處理注釋節(jié)點 break case Static://靜態(tài)類 if (n1 == null) {//如果老節(jié)點不存在 mountStaticNode(n2, container, anchor, isSVG)//掛載靜態(tài)節(jié)點 } 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( //懸念處理 ) } } // 設(shè)置 參考 ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } }
patch函數(shù)可見,主要做的就是 新舊虛擬節(jié)點之間的對比,這也是常說的diff算法,結(jié)合render(vnode, rootContainer, isSVG)
可以看出vnode對應的是n1也就是新節(jié)點,而rootContainer對應n2,也就是老節(jié)點。其做的邏輯判斷是。
- 新舊節(jié)點相同則直接返回
- 舊節(jié)點存在,且新節(jié)點和舊節(jié)點的類型不同,舊節(jié)點將被卸載
unmount
且復位清空null
。錨點移向下個節(jié)點。 - 新節(jié)點是否是動態(tài)值優(yōu)化標記
- 對新節(jié)點的類型判斷
- 文本類:
processText
- 注釋類:
processComment
- 靜態(tài)類:
mountStaticNode
- 片段類:
processFragment
- 默認
- 文本類:
而這個默認才是主要的部分也是最常用到的部分。里面包含了對類型是元素element
、組件component
、傳送teleport
、懸念suspense
的處理。這次主要講的是虛擬節(jié)點到組件和普通元素渲染的過程,其他類型的暫時不提,內(nèi)容展開過于雜亂。
實際上第一次初始運行的時候,patch判斷vnode類型根節(jié)點,因為vue3書寫的時候,都是以組件的形式體現(xiàn),所以第一次的類型勢必是component類型。
processComponent 節(jié)點類型是組件下的處理
const processComponent = ( n1: VNode | null,//老節(jié)點 n2: VNode,//新節(jié)點 container: RendererElement,//宿主 anchor: RendererNode | null,//錨點 parentComponent: ComponentInternalInstance | null,//父組件 parentSuspense: SuspenseBoundary | null,//父懸念 isSVG: boolean, slotScopeIds: string[] | null,//插槽 optimized: boolean ) => { n2.slotScopeIds = slotScopeIds if (n1 == null) {//如果老節(jié)點不存在,初次渲染的時候 //省略一部分n2其他情況下的處理 //掛載組件 mountComponent( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { //更新組件 updateComponent(n1, n2, optimized) } }
老節(jié)點n1不存在null
的時候,將掛載n2節(jié)點。如果老節(jié)點存在的時候,則更新組件。因此mountComponent()
最常見的就是在首次渲染的時候,那時舊節(jié)點都是空的。
接下來就是看如何掛載組件mountComponent()
const mountComponent: MountComponentFn = ( initialVNode,//對應n2 新的節(jié)點 container,//對應宿主 anchor,//錨點 parentComponent,//父組件 parentSuspense,//父傳送 isSVG,//是否SVG optimized//是否優(yōu)化 ) => { // 2.x編譯器可以在實際安裝前預先創(chuàng)建組件實例。 const compatMountInstance = //判斷是不是根組件且是組件 __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component const instance: ComponentInternalInstance = compatMountInstance || //創(chuàng)建組件實例 (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )) // 如果新節(jié)點是緩存組件的話那么將internals賦值給期渲染函數(shù) if (isKeepAlive(initialVNode)) { ;(instance.ctx as KeepAliveContext).renderer = internals } // 為了設(shè)置上下文處理props和slot插槽 if (!(__COMPAT__ && compatMountInstance)) { //設(shè)置組件實例 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 } //設(shè)置運行渲染副作用函數(shù) setupRenderEffect( instance,//存儲了新節(jié)點的組件上下文,props插槽等其他實例屬性 initialVNode,//新節(jié)點n2 container,//容器 anchor,//錨點 parentSuspense,//父懸念 isSVG,//是否SVG optimized//是否優(yōu)化 ) }
掛載組件中,除開緩存和懸掛上的函數(shù)處理,其邏輯上基本為:創(chuàng)建組件的實例createComponentInstance()
,設(shè)置組件實例 setupComponent(instance)
和設(shè)置運行渲染副作用函數(shù)setupRenderEffect()
。
創(chuàng)建組件實例,基本跟創(chuàng)建虛擬節(jié)點一樣的,內(nèi)部以對象的方式創(chuàng)建渲染組件實例。 設(shè)置組件實例,是將組件中許多數(shù)據(jù),賦值給了instance,維護組件上下文,同時對props和插槽等屬性初始化處理。
然后是setupRenderEffect
設(shè)置渲染副作用函數(shù);
const setupRenderEffect: SetupRenderEffectFn = ( instance,//實例 initialVNode,//初始化節(jié)點 container,//容器 anchor,//錨點 parentSuspense,//父懸念 isSVG,//是否是SVG optimized//優(yōu)化標記 ) => { //組件更新方法 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é)點 toggleRecurse(instance, true) //這部分是跟服務器渲染相關(guān)的邏輯處理 //創(chuàng)建子樹,同時 const subTree = (instance.subTree = renderComponentRoot(instance)) //遞歸 patch( null,//因為是掛載,所以n1這個老節(jié)點是空的。 subTree,//子樹賦值到n2這個新節(jié)點 container,//掛載到container上 anchor, instance, parentSuspense, isSVG ) //保留渲染生成的子樹DOM節(jié)點 initialVNode.el = subTree.el // 已掛載鉤子 // 掛在后的節(jié)點 //激活為了緩存根的鉤子 // #1742 激活的鉤子必須在第一次渲染后被訪問 因為該鉤子可能會被子類的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)建用于渲染的響應式副作用 const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, () => queueJob(update), instance.scope // 在組件的效果范圍內(nèi)跟蹤它 )) //更新方法 const update: SchedulerJob = (instance.update = () => effect.run()) //實例的uid賦值給更新的id update.id = instance.uid // 允許遞歸 // #1801, #2043 組件渲染效果應允許遞歸更新 toggleRecurse(instance, true) update() }
setupRenderEffect()
最后執(zhí)行的了 update()
方法,其實是運行了effect.run()
,并且將其賦值給了instance.updata中。而 effect 涉及到了 vue3 的響應式模塊,該模塊的主要功能就是,讓對象屬性具有響應式功能,當其中的屬性發(fā)生了變動,那effect副作用所包含的函數(shù)也會重新執(zhí)行一遍,從而讓界面重新渲染。這一塊內(nèi)容先不管。從effect函數(shù)看,明白了調(diào)用了componentUpdateFn
, 即組件更新方法,這個方法涉及了2個條件,一個是初次運行的掛載,而另一個是節(jié)點變動后的更新組件。 componentUpdateFn
中進行的初次渲染,主要是生成了subTree
然后把subTree
傳遞到patch進行了遞歸掛載到container上。
subTree是什么?
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> 節(jié)點渲染渲染生成的vnode就是 helloWorld組件的initialVNode,而這個組件內(nèi)部所有的DOM節(jié)點就是vnode通過執(zhí)行renderComponentRoot
渲染生成的的subTree。 每個組件渲染的時候都會運行render函數(shù),renderComponentRoot
就是去執(zhí)行render函數(shù)創(chuàng)建整個組件內(nèi)部的vnode,然后進行標準化就得到了該函數(shù)的返回結(jié)果:子樹vnode。 生成子樹后,接下來就是繼續(xù)調(diào)用patch函數(shù)把子樹vnode掛載到container上去。 回到patch后,就會繼續(xù)對子樹vnode進行判斷,例如上面的App組件的根節(jié)點是<div>標簽,而對應的subTree就是普通元素vnode,接下來就是堆普通Element處理的流程。
當節(jié)點的類型是普通元素DOM時候,patch判斷運行processElement
const processElement = ( n1: VNode | null, //老節(jié)點 n2: VNode,//新節(jié)點 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) {//如果沒有老節(jié)點,其實就是初次渲染,則運行mountElement mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { //如果是更新節(jié)點則運行patchElement patchElement( n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }
邏輯依舊,如果有n1老節(jié)點為null的時候,運行掛載元素的邏輯,否則運行更新元素節(jié)點的方法。
以下是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é)點 el = vnode.el = hostCreateElement( vnode.type as string, isSVG, props && props.is, props ) // 首先掛載子類,因為某些props依賴于子類內(nèi)容 // 已經(jīng)渲染, 例如 `<select value>` // 如果標記判斷子節(jié)點類型是文本類型 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 處理子節(jié)點是純文本的情況 hostSetElementText(el, vnode.children as string) //如果標記類型是數(shù)組子類 } 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') } // 設(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)部懸念+懸念未解決的情況,進入鉤子應該在懸念解決時調(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é)點,處理節(jié)點子節(jié)點,掛載子節(jié)點,同時對props相關(guān)處理。
所以根據(jù)代碼,首先是通過hostCreateElement方法創(chuàng)建了DOM元素節(jié)點。
const {createElement:hostCreateElement } = options
是從options這個實參中解構(gòu)并重命名為hostCreateElement
方法的,那么這個實參是從哪里來 需要追溯一下,回到初次渲染開始的流程中去。
從這流程圖可以清楚的知道,options
中createElement
方法是從nodeOps.ts
文件中導出的并傳入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 },
從中可以看出,其實是調(diào)用了底層的DOM API document.createElement創(chuàng)建元素。
說回上面,創(chuàng)建完DOM節(jié)點元素之后,接下來是繼續(xù)判斷子節(jié)點的類型,如果子節(jié)點是文本類型的,則調(diào)用處理文本hostSetElementText()
方法。
const {setElementText: hostSetElementText} = option setElementText: (el, text) => { el.textContent = text },
與前面的createElement一樣,setElementText方法是通過設(shè)置DOM元素的textContent屬性設(shè)置文本。
而如果子節(jié)點的類型是數(shù)組類,則執(zhí)行mountChildren方法,對子節(jié)點進行掛載:
const mountChildren: MountChildrenFn = ( children,//子節(jié)點數(shù)組里的內(nèi)容 container,//容器 anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized,//優(yōu)化標記 start = 0 ) => { //遍歷子節(jié)點中的內(nèi)容 for (let i = start; i < children.length; i++) { //根據(jù)優(yōu)化標記進行判斷進行克隆或者節(jié)點初始化處理。 const child = (children[i] = optimized ? cloneIfMounted(children[i] as VNode) : normalizeVNode(children[i])) //執(zhí)行patch方法,遞歸掛載child patch( null,//因為是初次掛載所以沒有老的節(jié)點 child,//虛擬子節(jié)點 container,//容器 anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }
子節(jié)點的掛載邏輯看起來會非常眼熟,在對children數(shù)組進行遍歷之后獲取到的每一個child,進行預處理后并對其執(zhí)行掛載方法。 結(jié)合之前調(diào)用mountChildren()
方法傳入的實參和其形參之間的對比。
mountChildren( vnode.children as VNodeArrayChildren, //節(jié)點中子節(jié)點的內(nèi)容 el,//DOM元素 null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized ) const mountChildren: MountChildrenFn = ( children,//子節(jié)點數(shù)組里的內(nèi)容 container,//容器 anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized,//優(yōu)化標記 start = 0 )
明確的對應上了第二個參數(shù)是container,而調(diào)用mountChildren
方法時傳入第二個參數(shù)的是在調(diào)用mountElement()
時創(chuàng)建的DOM節(jié)點,這樣便建立起了父子關(guān)系。 而且,后續(xù)的繼續(xù)遞歸patch()
,能深度遍歷樹的方式,可以完整的把DOM樹遍歷出來,完成渲染。
處理完節(jié)點的后,最后會調(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é)點的掛載。
注意點:node.insertBefore(newnode,existingnode)中_existingnode_雖然是可選的對象,但是實際上,在不同的瀏覽器會有不同的表現(xiàn)形式,所以如果沒有existingnode值的情況下,填入null會將新的節(jié)點添加到node子節(jié)點的尾部。
以上就是Vue3將虛擬節(jié)點渲染到網(wǎng)頁初次渲染詳解的詳細內(nèi)容,更多關(guān)于Vue3虛擬節(jié)點渲染網(wǎng)頁的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vuejs數(shù)據(jù)超出單行顯示更多,點擊展開剩余數(shù)據(jù)實例
這篇文章主要介紹了vuejs數(shù)據(jù)超出單行顯示更多,點擊展開剩余數(shù)據(jù),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-05-05vue中使用elementui實現(xiàn)樹組件tree右鍵增刪改功能
這篇文章主要介紹了vue中使用elementui實現(xiàn)對樹組件tree右鍵增刪改功能,右擊節(jié)點可進行增刪改,對節(jié)點數(shù)據(jù)進行模糊查詢功能,本文給大家分享了完整代碼,需要的朋友可以參考下2022-08-08vue.js組件vue-waterfall-easy實現(xiàn)瀑布流效果
這篇文章主要為大家詳細介紹了vue.js實現(xiàn)瀑布流之vue-waterfall-easy的相關(guān)資料,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08vue使用element-resize-detector監(jiān)聽元素寬度變化方式
這篇文章主要介紹了vue使用element-resize-detector監(jiān)聽元素寬度變化方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12vue中nextTick函數(shù)和react類似實現(xiàn)代碼
Vue 3 中的 nextTick 主要通過 Promise 實現(xiàn)異步調(diào)度,返回一個 Promise 對象,這篇文章主要介紹了vue中nextTick函數(shù)和react類似實現(xiàn)代碼,需要的朋友可以參考下2024-04-04