自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Vue3源碼解析計(jì)劃之組件渲染,VNode如何轉(zhuǎn)為真實(shí)DOM

開發(fā) 前端
在VUE中,組件是一個(gè)非常重要的概念,整個(gè)應(yīng)用的頁(yè)面都是通過(guò)組件進(jìn)行渲染實(shí)現(xiàn)的,但是我們?cè)诰帉懡M件時(shí),它們內(nèi)部又是如何進(jìn)行工作的呢?

[[439941]]

1寫在前面

在VUE中,組件是一個(gè)非常重要的概念,整個(gè)應(yīng)用的頁(yè)面都是通過(guò)組件進(jìn)行渲染實(shí)現(xiàn)的,但是我們?cè)诰帉懡M件時(shí),它們內(nèi)部又是如何進(jìn)行工作的呢?從我們開始編寫組件,到最終轉(zhuǎn)為真實(shí)DOM,是一個(gè)怎樣的轉(zhuǎn)變過(guò)程呢?那么我們應(yīng)該先來(lái)了解vue3中組件時(shí)如何渲染的?

2組件

組件是一個(gè)抽象概念,它是對(duì)一棵DOM樹的抽象,在頁(yè)面寫一個(gè)組件節(jié)點(diǎn):,它并不會(huì)在頁(yè)面上渲染這個(gè)叫的標(biāo)簽。我們?cè)趯懡M件時(shí),應(yīng)該內(nèi)部時(shí)這樣的:

  1. <template> 
  2.     <div class="test"
  3.     <p>hello world</p> 
  4.   </div>   
  5. </template> 

那么,一個(gè)組件想要真正渲染成DOM需要以下幾個(gè)步驟:

  • 創(chuàng)建VNode
  • 渲染VNode
  • 生成真實(shí)DOM

這里的VNode是什么,其實(shí)就是能夠描述組件信息的Javascript對(duì)象。

3應(yīng)用程序初始化

一個(gè)組件可以通過(guò)"模板+對(duì)象描述"的方式創(chuàng)建組件,創(chuàng)建好后又是如何被調(diào)用并進(jìn)行初始化的呢?

因?yàn)檎麄€(gè)組件樹是從根組件開始進(jìn)行渲染的,要尋找到根組件的渲染入口,需要從應(yīng)用程序的初始化過(guò)程開始分析。

我們分別看下vue2和vue3初始化應(yīng)用代碼有啥區(qū)別,但其實(shí)沒多大區(qū)別。

  1. //vue2  
  2. import Vue from "vue"
  3. import App from "./App"
  4.  
  5. const app = new Vue({ 
  6.     render:h=>h(App); 
  7. }) 
  8.  
  9. app.$mount("#app"); 
  10.  
  11. //vue3 
  12. import {createApp} from "vue"
  13. import App from "./app"
  14. const app = createApp(App); 
  15. app.mount("#app"); 

接下來(lái)我們看看createApp內(nèi)部實(shí)現(xiàn):

  1. export const createApp = ((...args) => { 
  2.   //創(chuàng)建app對(duì)象 
  3.   const app = ensureRenderer().createApp(...args) 
  4.  
  5.   if (__DEV__) { 
  6.     injectNativeTagCheck(app) 
  7.   } 
  8.  
  9.   const { mount } = app 
  10.     //重寫mount方法 
  11.   app.mount = (containerOrSelector: Element | string): any => { 
  12.     const container = normalizeContainer(containerOrSelector) 
  13.     if (!container) return 
  14.     const component = app._component 
  15.     if (!isFunction(component) && !component.render && !component.template) { 
  16.       component.template = container.innerHTML 
  17.     } 
  18.     // clear content before mounting 
  19.     container.innerHTML = '' 
  20.     const proxy = mount(container) 
  21.     container.removeAttribute('v-cloak'
  22.     return proxy 
  23.   } 
  24.  
  25.   return app 
  26. }) as CreateAppFunction<Element> 

我們看到const app = ensureRenderer().createApp(...args)用來(lái)創(chuàng)建app對(duì)象,那么其內(nèi)部是如何實(shí)現(xiàn)的:

  1. //渲染相關(guān)的一些配置,比如:更新屬性的方法,操作DOM的方法 
  2. const rendererOptions = { 
  3.   patchProp,  // 處理 props 屬性  
  4.   ...nodeOps // 處理 DOM 節(jié)點(diǎn)操作 
  5.  
  6. // lazy create the renderer - this makes core renderer logic tree-shakable 
  7. // in case the user only imports reactivity utilities from Vue. 
  8.  
  9. let renderer: Renderer | HydrationRenderer 
  10.  
  11. let enabledHydration = false 
  12. // 我們看到中文翻譯就是:延時(shí)創(chuàng)建渲染器,當(dāng)用戶只依賴響應(yīng)式包的時(shí)候,不會(huì)立即創(chuàng)建渲染器, 
  13. // 可以通過(guò)tree-shakable移除核心渲染邏輯相關(guān)的代碼 
  14. function ensureRenderer() { 
  15.   return renderer || (renderer = createRenderer(rendererOptions)) 

渲染器,這是為了跨平臺(tái)渲染做準(zhǔn)備的,簡(jiǎn)單理解就是:包含平臺(tái)渲染邏輯的js對(duì)象。我們看到創(chuàng)建渲染器,是通過(guò)調(diào)用createRenderer來(lái)實(shí)現(xiàn)的,其通過(guò)調(diào)用baseCreateRenderer函數(shù)進(jìn)行返回,其中就有我們要找的createApp: createAppAPI(render, hydrate)。

  1. export function createRenderer< 
  2.   HostNode = RendererNode, 
  3.   HostElement = RendererElement 
  4. >(options: RendererOptions<HostNode, HostElement>) { 
  5.   return baseCreateRenderer<HostNode, HostElement>(options) 
  6.  
  7. // 
  8. function baseCreateRenderer( 
  9.   options: RendererOptions, 
  10.   createHydrationFns?: typeof createHydrationFunctions 
  11. ): any { 
  12.   const { 
  13.     insert: hostInsert, 
  14.     remove: hostRemove, 
  15.     patchProp: hostPatchProp, 
  16.     createElement: hostCreateElement, 
  17.     createText: hostCreateText, 
  18.     createComment: hostCreateComment, 
  19.     setText: hostSetText, 
  20.     setElementText: hostSetElementText, 
  21.     parentNode: hostParentNode, 
  22.     nextSibling: hostNextSibling, 
  23.     setScopeId: hostSetScopeId = NOOP, 
  24.     cloneNode: hostCloneNode, 
  25.     insertStaticContent: hostInsertStaticContent 
  26.   } = options 
  27.  
  28.   // ....此處省略兩千行,我們先不管 
  29.  
  30.   return { 
  31.     render, 
  32.     hydrate, 
  33.     createApp: createAppAPI(render, hydrate) 
  34.   } 

我們看到createAppAPI(render, hydrate)方法接受兩個(gè)參數(shù):根組件渲染函數(shù)render,可選參數(shù)hydrate是在SSR場(chǎng)景下應(yīng)用的,這里先不關(guān)注。

  1. export function createAppAPI<HostElement>( 
  2.   render: RootRenderFunction, 
  3.   hydrate?: RootHydrateFunction 
  4. ): CreateAppFunction<HostElement> { 
  5.   //createApp方法接受的兩個(gè)參數(shù):根組件的對(duì)象和prop 
  6.   return function createApp(rootComponent, rootProps = null) { 
  7.     if (rootProps != null && !isObject(rootProps)) { 
  8.       __DEV__ && warn(`root props passed to app.mount() must be an object.`) 
  9.       rootProps = null 
  10.     } 
  11.  
  12.     // 創(chuàng)建默認(rèn)APP配置 
  13.     const context = createAppContext() 
  14.     const installedPlugins = new Set() 
  15.  
  16.     let isMounted = false 
  17.  
  18.     const app: App = { 
  19.       _component: rootComponent as Component, 
  20.       _props: rootProps, 
  21.       _container: null
  22.       _context: context, 
  23.  
  24.       get config() { 
  25.         return context.config 
  26.       }, 
  27.  
  28.       set config(v) { 
  29.         if (__DEV__) { 
  30.           warn( 
  31.             `app.config cannot be replaced. Modify individual options instead.` 
  32.           ) 
  33.         } 
  34.       }, 
  35.  
  36.       // 都是一些眼熟的方法 
  37.       use() {}, 
  38.       mixin() {}, 
  39.       component() {}, 
  40.       directive() {}, 
  41.       //用于掛載組件 
  42.       mount(rootContainer){ 
  43.         //創(chuàng)建根組件的VNode 
  44.         const vnode = createVNode(rootComponent,rootProps); 
  45.         //利用渲染器渲染VNode 
  46.         render(vnode,rootContainer); 
  47.         app._container = rootComponent; 
  48.         return vnode.component.proxy; 
  49.       } 
  50.  
  51.       // ... 
  52.     } 
  53.      
  54.      
  55.     return app 
  56.   } 

在整個(gè)app對(duì)象的創(chuàng)建過(guò)程中,vue.js利用 閉包和函數(shù)柯里化 的技巧,很好的實(shí)現(xiàn)參數(shù)保留。如:在執(zhí)行app.mount的時(shí)候,不需要傳入渲染器render,因?yàn)樵趫?zhí)行createAppAPI的時(shí)候,渲染器render參數(shù)已經(jīng)被保留下來(lái)。

我們知道在vue源碼中已經(jīng)將mount方法已經(jīng)進(jìn)行封裝,但是在我們使用時(shí)為什么還要進(jìn)行重寫,而不是直接把相關(guān)邏輯放在app對(duì)象的mount方法內(nèi)部實(shí)現(xiàn)呢?

重寫的目的是:實(shí)現(xiàn)既能讓用戶在使用API時(shí)更加靈活,也可以兼容Vue2的寫法。

這是因?yàn)関ue.js不僅僅是為web平臺(tái)服務(wù)的,其設(shè)計(jì)的目標(biāo)是"星辰大海"--實(shí)現(xiàn)支持跨平臺(tái)渲染,內(nèi)部不能夠包含任何指定平臺(tái)的內(nèi)容,createApp函數(shù)內(nèi)部的app.mount方法是一個(gè)標(biāo)準(zhǔn)的可跨平臺(tái)的組件渲染流程:先創(chuàng)建VNode,再渲染VNode。

  1. mount(rootContainer){ 
  2.   //創(chuàng)建根組件的VNode 
  3.   const vnode = createVNode(rootComponent,rootProps); 
  4.   //利用渲染器渲染VNode 
  5.   render(vnode,rootContainer); 
  6.   app._container = rootComponent; 
  7.   return vnode.component.proxy; 

我們看到app.mount重寫的代碼如下:

  1. //重寫mount方法 
  2. app.mount = (containerOrSelector: Element | string): any => { 
  3.   //標(biāo)準(zhǔn)化容器 
  4.   const container = normalizeContainer(containerOrSelector) 
  5.   //如果容器為空對(duì)象,就直接返回呢 
  6.   if (!container) return 
  7.   const component = app._component 
  8.   //如果組件對(duì)象沒有定義render函數(shù)和template模板,則直接取出容器的innerHTML方法作為組件模板內(nèi)容 
  9.   if (!isFunction(component) && !component.render && !component.template) { 
  10.     component.template = container.innerHTML 
  11.   } 
  12.   //在掛載前清空容器內(nèi)容 clear content before mounting 
  13.   container.innerHTML = '' 
  14.   //實(shí)現(xiàn)真正的掛載 
  15.   const proxy = mount(container) 
  16.   container.removeAttribute('v-cloak'
  17.   return proxy 

4核心渲染流程:創(chuàng)建VNode和渲染VNode

vnode本質(zhì)上用來(lái)描述DOM的Javascript對(duì)象,它在vue中可以描述不同節(jié)點(diǎn),比如:普通元素節(jié)點(diǎn)、組件節(jié)點(diǎn)等。

我們可以使用vnode來(lái)表示button標(biāo)簽:

  • type:標(biāo)簽的類型
  • props:標(biāo)簽的DOM屬性信息
  • children:DOM的子節(jié)點(diǎn),vnode數(shù)組
  1. const vnode = { 
  2.   //標(biāo)簽的類型 
  3.  type:"button"
  4.   //標(biāo)簽的DOM屬性信息 
  5.   props:{ 
  6.    "class":"btn"
  7.     style:{ 
  8.      width:"100px"
  9.       height:"100px" 
  10.     } 
  11.   }, 
  12.   //dom的子節(jié)點(diǎn),vnode數(shù)組 
  13.   children:"確認(rèn)" 

那么,我們可以使用vnode來(lái)對(duì)抽象事物的描述,比如用來(lái)表示組件標(biāo)簽,頁(yè)面并不會(huì)真正渲染一個(gè)叫做HelloWorld的標(biāo)簽元素,而是渲染組件內(nèi)部定義的原生的HTML標(biāo)簽元素。

  1. const HelloWorld = { 
  2.  //定義組件對(duì)象信息 
  3.  
  4. const vnode = { 
  5.  type:HelloWorld, 
  6.   props:{ 
  7.    msg:"test" 
  8.   } 

我們?cè)谙耄簐node到底有什么優(yōu)勢(shì),為什么一定要設(shè)計(jì)成vnode這樣的數(shù)據(jù)結(jié)構(gòu)?

  • 抽象:引入vnode,可以將渲染過(guò)程抽象化,從而使得組件的抽象能力有所提升。
  • 跨平臺(tái):因?yàn)閜atch vnode過(guò)程不同平臺(tái)可以有自己的實(shí)現(xiàn),給予vnode再做服務(wù)端渲染、weex平臺(tái)、小程序平臺(tái)的渲染。

但是呢,注意:使用vnode并不意味著不用操作真實(shí)DOM。很多人會(huì)誤認(rèn)為vnode的性能一定會(huì)比手動(dòng)操作DOM好,但其實(shí)并不是一定的。這是因?yàn)椋?/p>

  • 基于vnode實(shí)現(xiàn)的MVVM框架,在每次render to vnode過(guò)程中,渲染組件會(huì)有一定的javascript耗時(shí),尤其是大組件
  • 當(dāng)我們?nèi)ジ陆M件時(shí),可以感覺到明顯的卡頓現(xiàn)象。雖然diff算法在減少DOM操作方面足夠優(yōu)秀,但最終還是免不了操作DOM,所以性能并不能說(shuō)是絕對(duì)優(yōu)勢(shì)

創(chuàng)建VNode

我們前面捋了一遍源碼,知道vue中是通過(guò)createVNode函數(shù)創(chuàng)建根組件的vnode的。

  1. const vnode = createVNode(rootComponent,rootProps); 
  2.  
  3. //createVNode函數(shù)的大致實(shí)現(xiàn)流程 
  4. function createVNode(type,props=null,children=null){ 
  5.  if(props){ 
  6.    //處理props的相關(guān)邏輯,標(biāo)準(zhǔn)化class和style 
  7.   } 
  8.   //對(duì)于vnode類型信息編碼 
  9.   const shapeFlag = isString(type)  
  10.   ? 1/*ELEMENT*/ : isSuspense(type)  
  11.   ? 128 /*SUSPENSE*/ : isTeleport(type) 
  12.   ? 64 /*TELEPORT*/ : isObject(type) 
  13.   ? 4 /*STATEFUL_COMPONENT*/ : isFunction(type) 
  14.   ? 2 /*FUNCTIONAL_COMPONENT*/ : 0 
  15.    
  16.   const vnode = { 
  17.    type, 
  18.     props, 
  19.     shapeFlag, 
  20.     //其他屬性 
  21.   } 
  22.    
  23.   //標(biāo)準(zhǔn)化子節(jié)點(diǎn),把不同數(shù)據(jù)類型的children轉(zhuǎn)成數(shù)組或文本類型 
  24.   normalizeChildren(vnode,children) 
  25.   return vnode 

渲染VNode

  1. render(vnode,rootContainer) 
  2. function render(vnode,rootContainer){ 
  3.  //判斷是否為空 
  4.   if(vnode == null){ 
  5.     //如果為空,執(zhí)行銷毀組件的邏輯 
  6.    if(container._vnode){ 
  7.      unmount(container._vnode,null,null,true
  8.     } 
  9.   }else
  10.    //創(chuàng)建或更新組件 
  11.     patch(container._vnode||null,vnode,container) 
  12.   } 
  13.   //緩存vnode節(jié)點(diǎn),表示已經(jīng)渲染 
  14.   container._vnode = vnode 

那么在渲染vnode過(guò)程中涉及道到的patch補(bǔ)丁函數(shù)是如何實(shí)現(xiàn)的:

  1. function patch( 
  2.  n1,//舊的vnode,當(dāng)n1==null時(shí),表示時(shí)一次掛載的過(guò)程 
  3.   n2,//新的vnode,后續(xù)會(huì)根據(jù)這個(gè)vnode類型執(zhí)行不同的處理邏輯 
  4.   container,//表示dom容器,在vnode渲染生成DOM后,會(huì)掛載到container下面 
  5.   anchor=null
  6.   parentComponent=null
  7.   parentSuspense=null
  8.   isSVG=false
  9.   optimized=false 
  10. ){ 
  11.  //如果存在新舊節(jié)點(diǎn),且新舊節(jié)點(diǎn)類型不同,則銷毀舊節(jié)點(diǎn) 
  12.   if(n1&&!isSameVNodeType(n1,n2)){ 
  13.    anchor = getNextHostNode(n1); 
  14.     unmount(n1,parentComponent,parentSuspense,true); 
  15.     n1 = null
  16.   } 
  17.   const {type,shapeFlag} = n2; 
  18.     switch(type){ 
  19.       case Test: 
  20.         //處理文本節(jié)點(diǎn) 
  21.        break 
  22.       case Comment: 
  23.         //處理注釋節(jié)點(diǎn) 
  24.         break 
  25.       case Static
  26.         //處理靜態(tài)節(jié)點(diǎn) 
  27.         break 
  28.       case Fragment: 
  29.         //處理Fragment元素 
  30.         break 
  31.       default
  32.         if(shapeFlag & 1 /*ELEMENT*/){ 
  33.          //處理普通DOM元素 
  34.           processElemnt( 
  35.             n1, 
  36.             n2, 
  37.             container, 
  38.             anchor, 
  39.             parentComponent, 
  40.             parentSuspense, 
  41.             isSVG, 
  42.             optimized 
  43.           ) 
  44.         }else if(shapeFlag & 64 /*TELEPORT*/){ 
  45.          //處理普通TELEPORT 
  46.           processElemnt( 
  47.             n1, 
  48.             n2, 
  49.             container, 
  50.             anchor, 
  51.             parentComponent, 
  52.             parentSuspense, 
  53.             isSVG, 
  54.             optimized 
  55.           ) 
  56.         }else if(){ 
  57.          
  58.         }else if(){ 
  59.          
  60.         }else if(){ 
  61.          
  62.         } 
  63.     } 

我們看下處理組件的parentComponent函數(shù)的實(shí)現(xiàn):

  1. function parentComponent( 
  2.   n1, 
  3.    n2, 
  4.    container, 
  5.    anchor, 
  6.    parentComponent, 
  7.    parentSuspense, 
  8.    isSVG, 
  9.    optimized 
  10. ){ 
  11.  if(n1==null){ 
  12.    //掛載組件 
  13.     mountComponent( 
  14.      n1, 
  15.       n2, 
  16.       container, 
  17.       anchor, 
  18.       parentComponent, 
  19.       parentSuspense, 
  20.       isSVG, 
  21.       optimized 
  22.     ) 
  23.   }else
  24.    //更新組件 
  25.     updateComponent( 
  26.      n1, 
  27.       n2, 
  28.       parentComponent, 
  29.       optimized 
  30.     ) 
  31.   }    

關(guān)于組件實(shí)例:

  • 創(chuàng)建組件實(shí)例:內(nèi)部通過(guò)對(duì)象的方式創(chuàng)建了當(dāng)前渲染的組件實(shí)例
  • 設(shè)置組件實(shí)例:instance保留了很多組件相關(guān)的數(shù)據(jù),維護(hù)了組件的上下文,包括對(duì)props、插槽以及其他實(shí)例的屬性的初始化處理

初始渲染主要做兩件事情:

  • 渲染組件生成subTree
  • 把subTree掛載到container中

再回到我們夢(mèng)開始的地方,我們看到在HelloWorld組件內(nèi)部,整個(gè)DOM節(jié)點(diǎn)對(duì)應(yīng)的vnode執(zhí)行renderComponentRoot渲染生成對(duì)應(yīng)的subTree,我們可以把它成為"子樹vnode"。

  1. <template> 
  2.     <div class="test">//test被稱為子樹vnode 
  3.     <p>hello world</p> 
  4.   </div>   
  5. </template> 

如果是其它平臺(tái)比如weex等,hostCreateElment方法就不再是操作DOM,而是平臺(tái)相關(guān)的API,這些平臺(tái)相關(guān)的方法是在創(chuàng)建渲染器階段作為參數(shù)傳入的。

創(chuàng)建完DOM節(jié)點(diǎn)后,要判斷如果有props,就給這個(gè)DOM節(jié)點(diǎn)添加相關(guān)的class、style、event等屬性,并在hostPatchProp函數(shù)內(nèi)部做相關(guān)的處理邏輯。

5嵌套組件

在生產(chǎn)開發(fā)中,App和hello組件的例子就是嵌套組件的場(chǎng)景,組件vnode主要維護(hù)著組件的定義對(duì)象,組件上的各種props,而組件本身是一個(gè)抽象節(jié)點(diǎn),它自身的渲染其實(shí)是通過(guò)執(zhí)行組件定義的render函數(shù)渲染生成的子樹vnode完成的,然后再通過(guò)patch這種遞歸方式,無(wú)論組件嵌套層級(jí)多深,都可以完成整個(gè)組件樹的渲染。

6參考文章

  • 《Vue3核心源碼解析》
  • 《Vue中文社區(qū)》
  • 《Vue3中文文檔》

7寫在最后

本文主要分析總結(jié)了組件的渲染流程,從入口開始層層分析組件渲染過(guò)程的源碼,我們知道了一個(gè)組件想要真正渲染成DOM需要以下三個(gè)步驟:

 

  • 創(chuàng)建VNode
  • 渲染VNode
  • 生成真實(shí)DOM

 

責(zé)任編輯:武曉燕 來(lái)源: 前端萬(wàn)有引力
相關(guān)推薦

2021-12-13 00:54:14

組件Vue3Setup

2021-12-14 21:43:13

Vue3函數(shù)computed

2021-02-26 05:19:20

Vue 3.0 VNode虛擬

2022-07-27 08:40:06

父子組件VUE3

2024-12-05 10:53:02

JSON數(shù)據(jù)服務(wù)器

2021-09-22 07:57:23

Vue3 插件Vue應(yīng)用

2023-11-28 09:03:59

Vue.jsJavaScript

2024-01-23 09:15:33

Vue3組件拖拽組件內(nèi)容編輯

2021-11-26 05:59:31

Vue3 插件Vue應(yīng)用

2022-08-08 08:03:44

MySQL數(shù)據(jù)庫(kù)CBO

2021-01-18 07:15:22

虛擬DOM真實(shí)DOMJavaScript

2024-08-13 09:26:07

2021-05-12 10:25:53

組件驗(yàn)證漏洞

2022-07-29 11:03:47

VueUni-app

2020-12-01 08:34:31

Vue3組件實(shí)踐

2021-12-02 05:50:35

Vue3 插件Vue應(yīng)用

2020-09-17 07:08:04

TypescriptVue3前端

2022-01-26 11:00:58

源碼層面Vue3

2021-12-01 08:11:44

Vue3 插件Vue應(yīng)用

2021-05-18 07:51:37

Suspense組件Vue3
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)