vue開(kāi)發(fā)runtime core中的虛擬節(jié)點(diǎn)示例詳解
引言
我們知道runtime-dom內(nèi)部功能其實(shí)是 將渲染時(shí)所需要節(jié)點(diǎn)操作的 API (即rendererOptions) 傳入到runtime-core中。
之前文章我們可以大概知道 runtime-dom 中節(jié)點(diǎn)操作屬性API有哪些,并試著實(shí)現(xiàn)部分API功能。runtime-core 是實(shí)現(xiàn)與平臺(tái)無(wú)關(guān)的運(yùn)行時(shí)功能(即runtime-core中的節(jié)點(diǎn)渲染)。
本文講述的內(nèi)容是:實(shí)現(xiàn) runtime-core 中的createAppAPI,完成虛擬節(jié)點(diǎn)的創(chuàng)建,以及render中的掛載所需參數(shù)的獲取。
將API傳入到runtime-core中
我們?cè)賮?lái)看下上篇文章的例子:
<div id="app"></div> <script src="./runtime-dom.global.js"></script> <script> let { createApp, h, ref } = VueRuntimeDOM function useCounter() { const count = ref(0) const add = () => { count.value++ } return { count, add } } let App = { props: { title: {} }, setup() { let { count, add } = useCounter() return { count, add } }, // 每次更新重新調(diào)用render方法 render(proxy) { return h('h2', { onClick: this.add, title: proxy.title }, 'hello dy' + this.count) } } let app = createApp(App, { title: 'dy' }) // 組件 組件參數(shù) app.mount('#app') </script>
用戶通過(guò)解析runtime-dom中的 createApp,在createApp中接收組件和組件傳入的屬性參數(shù),對(duì)根節(jié)點(diǎn)進(jìn)行初始化渲染。
那這樣的話,我們?cè)趓untime-dom需要對(duì)外暴露一個(gè)createApp方法,內(nèi)部實(shí)現(xiàn)根據(jù)相應(yīng)參數(shù)實(shí)現(xiàn)真實(shí)節(jié)點(diǎn)的初始化工作。
/** * * @param component 組件 * @param rootProps 傳入的屬性 */ export const createApp = (component, rootProps = null) => { } // 將runtime-core中所有API導(dǎo)出 export * from '@vue/runtime-core'
createRenderer 初始化
此時(shí),我們就需要?jiǎng)?chuàng)建一個(gè)渲染器createRenderer。在渲染器中實(shí)現(xiàn)一個(gè)mount掛載的方法??聪旅娴慕Y(jié)構(gòu):
export function createRenderer(rendererOptions) { // 虛擬節(jié)點(diǎn)轉(zhuǎn)化成真實(shí)節(jié)點(diǎn) 渲染到容器中 const render = (vnode, container) => { } // 創(chuàng)建節(jié)點(diǎn)相關(guān)的api mount use mixin const createAppAPI = (render) => { return (rootComponent, rootProps) => { let app = { mount(container) { }, use() { }, mixin() { }, component() { } } return app } } return { render, createApp: createAppAPI(render) } }
createAppAPI中是組件相關(guān)的API,比如mount、use、mixin等,在createAppAPI內(nèi)部我們肯定還需要元素掛載實(shí)現(xiàn)的render方法。
我們可以看到官網(wǎng)中的createRenderer API,中對(duì)外暴露了 render 和 createApp兩種方法,其實(shí)runtime-core中內(nèi)部也是這樣實(shí)現(xiàn)的。
當(dāng)然我們只是先將空架子搭建起來(lái),后面還需要一步步填充功能。
createApp 內(nèi)部實(shí)現(xiàn)
把上面例子來(lái)出來(lái):
let app = createApp(App, { title: 'dy' }) // 組件 組件參數(shù) app.mount('#app')
繼續(xù)回到createApp這個(gè)方法:
createRenderer實(shí)現(xiàn)一個(gè)渲染器,我們通過(guò) createRenderer 可以解構(gòu)其中對(duì)外暴露的方法,在 createApp 內(nèi)部進(jìn)行初始化,最后將其返回,用戶再通過(guò)mount對(duì)元素進(jìn)行掛載。
const { createApp } = createRenderer(rendererOptions) let app = createApp(component, rootProps) // 渲染器完成 將其返回
在初始化的時(shí)候我們通過(guò)用戶傳入的#app
,進(jìn)行初始化。在執(zhí)行用戶mount前,我們?cè)赾reateApp中存在實(shí)際元素掛載功能的mount,我們就需要先將其保存下來(lái),在執(zhí)行用戶mount時(shí),在其內(nèi)部進(jìn)行實(shí)際的元素掛載(即app.mount內(nèi)部執(zhí)行我們實(shí)際mount功能)。
createApp內(nèi)部:
let { mount } = app // 自己的 app.mount = (containerOrSelector) => { // 用戶傳入的 #app const container = nodeOps.querySelector(containerOrSelector) if (!container) return container.innerHTML = '' mount(container) }
在掛載的時(shí)候,我們需要清空當(dāng)前節(jié)點(diǎn)的內(nèi)部元素,再將其掛載。
完整的createApp:
export const createApp = (component, rootProps = null) => { const { createApp } = createRenderer(rendererOptions) // 將core中的createApp解構(gòu)出來(lái) // 創(chuàng)建一個(gè)渲染器 把功能傳遞給runtime-core let app = createApp(component, rootProps) let { mount } = app // 調(diào)用的是core中的mount // app.mount 用戶初始化的mount ==> app.mount("#app"); // 在用戶的app.mount中再去執(zhí)行core中的mount ==> core中就會(huì)存在domAPI 需要渲染的組件 目標(biāo)屬性,最后再去掛載到容器中 /** * containerOrSelector 用戶傳入的容器 */ app.mount = (containerOrSelector) => { // 用戶傳入的 #app const container = nodeOps.querySelector(containerOrSelector) if (!container) return // 不為空直接返回 // 在掛載之前清空 container.innerHTML = '' // 調(diào)用的是core中的mount 處理節(jié)點(diǎn)后將container傳入mount mount(container) } return app }
這樣我們 createRenderer 就可以拿到節(jié)點(diǎn)操作的DOM API(rendererOptions
)、需要渲染的組件(component
)、目標(biāo)屬性(rootProps
)和需要掛載的位置-容器(container
)。createRenderer也就可以獨(dú)立出來(lái),放到runtime-core中,在core中我們單獨(dú)去實(shí)現(xiàn)元素的渲染。
runtime-core
視角轉(zhuǎn)到我們core中,在core中我們需要實(shí)現(xiàn)一個(gè)渲染器。目錄結(jié)構(gòu)如下:
我們主要實(shí)現(xiàn)createRenderer這個(gè)功能對(duì)外暴露的兩個(gè)API,可以先來(lái)看看createApp中的 createAppAPI 。
createAppAPI
在 createAppAPI 中,我們目前需要實(shí)現(xiàn)真實(shí)元素的掛載。
說(shuō)的通俗一點(diǎn):掛載的核心就是根據(jù)傳入的組件對(duì)象,創(chuàng)造一個(gè)組件的虛擬節(jié)點(diǎn),再將這個(gè)虛擬節(jié)點(diǎn)渲染到容器中。
具體分兩步走:
mount(container) { // 1:創(chuàng)建組件虛擬節(jié)點(diǎn) const vnode = cerateVNode(rootComponent, rootProps)// h函數(shù) // 2:虛擬節(jié)點(diǎn)渲染到容器中 render(vnode, container) }
cerateVNode 創(chuàng)建組件虛擬節(jié)點(diǎn)
我們首先要知道,虛擬節(jié)點(diǎn)是一個(gè)數(shù)據(jù)對(duì)象,用一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)對(duì)元素進(jìn)行描述,當(dāng)前元素是組件還是元素、它的字節(jié)點(diǎn)、攜帶的參數(shù)等有哪些。
類型表示
我們需要判斷傳過(guò)來(lái)的type是組件還是元素,改如何表示呢???????
在源碼中,通過(guò)ShapeFlags
這個(gè)枚舉類就已經(jīng)將類型描述的非常清楚了。我們可以查看源碼中的packages/shared/src/shapeFlags.ts
這個(gè)文件:??????
export const enum ShapeFlags { ELEMENT = 1, // 元素 FUNCTIONAL_COMPONENT = 1 << 1, // 函數(shù)式組件 010 STATEFUL_COMPONENT = 1 << 2, // 普通組件 0100 TEXT_CHILDREN = 1 << 3, // children 是文本 01000 ARRAY_CHILDREN = 1 << 4, // children 是 數(shù)組 SLOTS_CHILDREN = 1 << 5, // children 是 插槽 TELEPORT = 1 << 6, // teleport 組件 SUSPENSE = 1 << 7, // suspense 組件 COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // keep-alive COMPONENT_KEPT_ALIVE = 1 << 9, // kept alive COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 組件 }
大部分類型我都進(jìn)行了注釋。
有人可能不太理解1 << 1
這種表達(dá)方式,這種是二進(jìn)制的表示方法,1 << 1
表示1向左移動(dòng)一位,實(shí)際值就是1,同樣,那么1 << 2
實(shí)際值就是4,依此類推。
這種寫法的好處就是,我們需要判斷一個(gè)值它是什么類型時(shí),可以直接與對(duì)應(yīng)的類型值相與。
舉個(gè)例子:
問(wèn):如何判斷當(dāng)前值011(3) 既是元素又是函數(shù)式組件?
理解:用當(dāng)前值011分別與ShapeFlags.ELEMENT、ShapeFlags.FUNCTIONAL_COMPONENT相與,如果不等于0就是包含。
答:
3與元素、函數(shù)式組件相與均不為0,所以011即時(shí)元素也是函數(shù)式組件。
同樣,如果想表示該類型即時(shí)元素也是函數(shù)式組件的話,我們可以將兩者類型進(jìn)行組合(即相或)。
ShapeFlags.ELEMENT | ShapeFlags.FUNCTIONAL_COMPONENT
就表示既是元素也是函數(shù)式組件。
虛擬節(jié)點(diǎn)創(chuàng)建
我們調(diào)用傳入的參數(shù)類型這種h函數(shù)接收的參數(shù):節(jié)點(diǎn)類型type、參數(shù)props、子節(jié)點(diǎn)children。
h('h2', { onClick: this.add, title: proxy.title }, 'hello dy' )
首先,我們需要判斷傳入過(guò)來(lái)的type是元素還是組件。我們這邊就先判斷是否是對(duì)象,如果是對(duì)象,那么節(jié)點(diǎn)類型就是組件,如果是字符串,那么該節(jié)點(diǎn)就是一個(gè)元素。
isObject(type) ? ShapeFlags.COMPONENT : isString(type) ? ShapeFlags.ELEMENT : 0
通過(guò)類型判斷,我們就可以拿到該節(jié)點(diǎn)的類型,除此之外,我們還需要它的參數(shù)、子節(jié)點(diǎn)、組件、真實(shí)節(jié)點(diǎn)等,并需要加上是否是虛擬節(jié)點(diǎn)的標(biāo)志。
之所以會(huì)說(shuō)runtime-core中是與平臺(tái)無(wú)關(guān)的運(yùn)行時(shí),是因?yàn)椴还苁菫g覽器、測(cè)試環(huán)境,都會(huì)先將其轉(zhuǎn)成可描述的虛擬節(jié)點(diǎn)vnode
,再去進(jìn)行節(jié)點(diǎn)對(duì)比、渲染等操作。
虛擬節(jié)點(diǎn)的創(chuàng)建功能:
export function cerateVNode(type, props, children = null) { // type如果是字符串 元素 ; type是對(duì)象或者函數(shù) == 組件 const shapeFlag = isObject(type) ? ShapeFlags.COMPONENT : isString(type) ? ShapeFlags.ELEMENT : 0 // 瀏覽器或者測(cè)試環(huán)境都會(huì)轉(zhuǎn)成vnode const vnode = { __v_isVNode: true, // 是否是虛擬節(jié)點(diǎn) type, // 節(jié)點(diǎn)類型 props, children, shapeFlag, key: props && props.key, // patch對(duì)比需要 component: null,// 如果是組件的虛擬節(jié)點(diǎn) 需要保存組件的實(shí)例 el: null//真實(shí)節(jié)點(diǎn) } if (children) { // 告訴節(jié)點(diǎn) 是什么樣的子節(jié)點(diǎn) // 稍后渲染虛擬節(jié)點(diǎn)的時(shí)候,可以判斷兒子是數(shù)組就循環(huán)渲染 vnode.shapeFlag = vnode.shapeFlag | (isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN) } // vnode 描述出來(lái)當(dāng)前是個(gè)什么節(jié)點(diǎn),包含的子節(jié)點(diǎn)的類型是什么 return vnode }
這樣我們創(chuàng)建出來(lái)的 vnode 就可以描述當(dāng)前的節(jié)點(diǎn)類型了。
render 虛擬節(jié)點(diǎn)渲染到容器中
我們拿到虛擬節(jié)點(diǎn)后,接下來(lái)要做的就是將其渲染到容器中,即 render 的實(shí)現(xiàn)。
與之前vue類似,通過(guò)patch來(lái)實(shí)現(xiàn)節(jié)點(diǎn)的渲染和更新。我們這次主要實(shí)現(xiàn)的是節(jié)點(diǎn)的初始化,所以patch傳入的老的節(jié)點(diǎn)應(yīng)該為null。
/** * 老的虛擬節(jié)點(diǎn) * 新的虛擬節(jié)點(diǎn) * 容器 */ patch(null, vnode, container)
通過(guò)傳入的新老vnode和當(dāng)前的容器,我們需要對(duì)新老虛擬節(jié)點(diǎn)進(jìn)行對(duì)比,然后判斷新節(jié)點(diǎn)的類型,進(jìn)行不同類型的節(jié)點(diǎn)創(chuàng)建。
const patch = (n1, n2, container) => { if (n1 === n2) return const { shapeFlag } = n2 // 傳入的是元素 if (shapeFlag & ShapeFlags.ELEMENT) { processElement(n1, n2, container) } // 組件 else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent(n1, n2, container) } }
目前,我們可以先以組件初始化為例:
const processComponent = (n1, n2, container) => { if (n1 === null) { // 組件初始化 mountComponent(n2, container) } else { // 組件的更新 } }
在處理組件的函數(shù)中判斷,如果沒(méi)有老虛擬節(jié)點(diǎn)則需要進(jìn)行組件初始化操作,否則我們就需要進(jìn)行組件的更新操作。
/** * * @param initialVNode 組件的虛擬節(jié)點(diǎn) * @param container 容器 */ const mountComponent = (initialVNode, container) => { // 組件的掛載 console.log(initialVNode, container); }
我們可以在組件初始化渲染中看看我們是否拿到上述案例中用戶傳入的虛擬節(jié)點(diǎn)和容器:
可以看到我們成功獲取到了當(dāng)前的虛擬節(jié)點(diǎn),此時(shí)的type
就是用戶傳入的對(duì)象props、setup、render;shapeFlag
是6,是因?yàn)楫?dāng)前節(jié)點(diǎn)是組件(函數(shù)式組件和普通組件的組合-ShapeFlags.COMPONENT)。
元素的掛載也是同樣的判斷流程:
我們也可以看到源碼中,不僅僅是對(duì)元素和組件情況,完整的vue框架還需要考慮節(jié)點(diǎn)類型是文本、注釋、靜態(tài)節(jié)點(diǎn)、代碼片段等等情況。
獲取到所需參數(shù)后,接下來(lái)的工作就是創(chuàng)建真實(shí)節(jié)點(diǎn)。將節(jié)點(diǎn)渲染到容器中,就需要用到我們之前實(shí)現(xiàn)的節(jié)點(diǎn)操作的API-rendererOptions。
// createRenderer函數(shù) const { insert: hostInsert, // 插入節(jié)點(diǎn) remove: hostRemove, // 刪除節(jié)點(diǎn) patchProp: hostPatchProp, createElement: hostCreateElement, // 創(chuàng)建元素 createText: hostCreateText, // 創(chuàng)建文本 createComment: hostCreateComment, // 注釋 setText: hostSetText, // 設(shè)置文本中的內(nèi)容 setElementText: hostSetElementText, // 設(shè)置文本內(nèi)容 parentNode: hostParentNode, // 獲取父節(jié)點(diǎn) nextSibling: hostNextSibling, // 兄弟節(jié)點(diǎn) setScopeId: hostSetScopeId = NOOP, // 設(shè)置scope id cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = rendererOptions
將rendererOptions
中的節(jié)點(diǎn)操作API結(jié)構(gòu)出來(lái),在組件、元素的掛載和更新函數(shù)中,根據(jù)不同情況創(chuàng)建元素節(jié)點(diǎn)、插入節(jié)點(diǎn)等DOM操作。
接下來(lái)的工作就是根據(jù)組件的虛擬節(jié)點(diǎn)創(chuàng)建一個(gè)真實(shí)節(jié)點(diǎn),渲染到容器中。在此主要分為兩步驟:
1、給組件創(chuàng)建一個(gè)組件的實(shí)例;
2、需要給組件的實(shí)例進(jìn)行賦值操作。然后render渲染到頁(yè)面上
以上就是vue開(kāi)發(fā)runtime core中的虛擬節(jié)點(diǎn)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于vue runtime core虛擬節(jié)點(diǎn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- vue3模塊創(chuàng)建runtime-dom源碼解析
- Vue完整版和runtime版的區(qū)別詳解
- Vue.js和Vue.runtime.js區(qū)別淺析
- vue-next/runtime-core 源碼閱讀指南詳解
- Vue中使用localStorage存儲(chǔ)token并設(shè)置時(shí)效
- vue中destroyed方法及使用示例講解
- vue中?render?函數(shù)功能與原理分析
- Vue中Number方法將字符串轉(zhuǎn)換為數(shù)字的過(guò)程
- Vue中 Runtime + Compiler 和 Runtime-only 兩種模式含義和區(qū)別詳解
相關(guān)文章
axios請(qǐng)求的一些常見(jiàn)操作實(shí)戰(zhàn)指南
axios是一個(gè)輕量的HTTP客戶端,它基于XMLHttpRequest服務(wù)來(lái)執(zhí)行 HTTP請(qǐng)求,支持豐富的配置,支持Promise,支持瀏覽器端和 Node.js 端,下面這篇文章主要給大家介紹了關(guān)于axios請(qǐng)求的一些常見(jiàn)操作,需要的朋友可以參考下2022-09-09如何在Vue單頁(yè)面中進(jìn)行業(yè)務(wù)數(shù)據(jù)的上報(bào)
為什么要在標(biāo)題里加上一個(gè)業(yè)務(wù)數(shù)據(jù)的上報(bào)呢,因?yàn)樵谠蹅兦岸隧?xiàng)目中,可上報(bào)的數(shù)據(jù)維度太多,比如還有性能數(shù)據(jù)、頁(yè)面錯(cuò)誤數(shù)據(jù)、console捕獲等。這里我們只講解業(yè)務(wù)數(shù)據(jù)的埋點(diǎn)。2021-05-05vue中watch監(jiān)聽(tīng)不到變化的解決
本文主要介紹了vue中watch監(jiān)聽(tīng)不到變化的解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01vue-quill-editor富文本編輯器上傳視頻功能詳解
需求需要實(shí)現(xiàn)富文本的功能,同時(shí)富文本中還可以上傳視頻和圖片,選來(lái)選去最后決定了用這個(gè)富文本編輯器,下面這篇文章主要給大家介紹了關(guān)于vue-quill-editor富文本編輯器上傳視頻功能的相關(guān)資料,需要的朋友可以參考下2023-05-05vue中el-tab如何點(diǎn)擊不同標(biāo)簽觸發(fā)不同函數(shù)的實(shí)現(xiàn)
el-tab本身的功能是點(diǎn)擊之后切換不同頁(yè),本文主要介紹了vue中el-tab如何點(diǎn)擊不同標(biāo)簽觸發(fā)不同函數(shù)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03this.$router.push攜帶參數(shù)跳轉(zhuǎn)頁(yè)面的實(shí)現(xiàn)代碼
這篇文章主要介紹了this.$router.push攜帶參數(shù)跳轉(zhuǎn)頁(yè)面,this.$router.push進(jìn)行頁(yè)面跳轉(zhuǎn)時(shí),攜帶參數(shù)有params和query兩種方式,本文結(jié)合實(shí)例代碼給大家詳細(xì)講解,需要的朋友可以參考下2023-04-04