溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

發(fā)布時(shí)間:2023-03-07 10:57:03 來(lái)源:億速云 閱讀:144 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹了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怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

何時(shí)會(huì)進(jìn)行虛擬函數(shù)的創(chuàng)建和渲染?

vue3初始化過(guò)程中,createApp()指向的源碼 core/packages/runtime-core/src/apiCreateApp.ts中

Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

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)行渲染。

什么是VNode?

虛擬節(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)的組件,子類是文本等。

前置須知

ShapeFlags

// 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)是什么類型。

為什么要使用Vnode?

因?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ù),但是始終需要操作。

Vnode是如何創(chuàng)建的?

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)建。

createBaseVNode 虛擬節(jié)點(diǎn)初始化創(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ò)程。

render 渲染 VNode

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)處理。

Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

 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)賦值到容器上
  }

patch VNode

這里來(lái)看一下有關(guān)patch()函數(shù)的代碼,側(cè)重了解當(dāng)組件初次渲染的時(shí)候的流程。

Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

// 注意:此閉包中的函數(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類型。

Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

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)不存在,初次渲染的時(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是什么?

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處理的流程。

當(dāng)節(jié)點(diǎn)的類型是普通元素DOM時(shí)候,patch判斷運(yùn)行processElement

Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

  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) 需要追溯一下,回到初次渲染開始的流程中去。

Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

從這流程圖可以清楚的知道,optionscreateElement方法是從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)的尾部。Vue3怎么將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁(yè)初次渲染

關(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è)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI