vue開發(fā)runtime core中的虛擬節(jié)點示例詳解
引言
我們知道runtime-dom內部功能其實是 將渲染時所需要節(jié)點操作的 API (即rendererOptions) 傳入到runtime-core中。
之前文章我們可以大概知道 runtime-dom 中節(jié)點操作屬性API有哪些,并試著實現(xiàn)部分API功能。runtime-core 是實現(xiàn)與平臺無關的運行時功能(即runtime-core中的節(jié)點渲染)。
本文講述的內容是:實現(xiàn) runtime-core 中的createAppAPI,完成虛擬節(jié)點的創(chuàng)建,以及render中的掛載所需參數(shù)的獲取。
將API傳入到runtime-core中
我們再來看下上篇文章的例子:
<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 }
},
// 每次更新重新調用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>
用戶通過解析runtime-dom中的 createApp,在createApp中接收組件和組件傳入的屬性參數(shù),對根節(jié)點進行初始化渲染。
那這樣的話,我們在runtime-dom需要對外暴露一個createApp方法,內部實現(xiàn)根據(jù)相應參數(shù)實現(xiàn)真實節(jié)點的初始化工作。
/**
*
* @param component 組件
* @param rootProps 傳入的屬性
*/
export const createApp = (component, rootProps = null) => { }
// 將runtime-core中所有API導出
export * from '@vue/runtime-core'
createRenderer 初始化
此時,我們就需要創(chuàng)建一個渲染器createRenderer。在渲染器中實現(xiàn)一個mount掛載的方法??聪旅娴慕Y構:
export function createRenderer(rendererOptions) {
// 虛擬節(jié)點轉化成真實節(jié)點 渲染到容器中
const render = (vnode, container) => {
}
// 創(chuàng)建節(jié)點相關的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中是組件相關的API,比如mount、use、mixin等,在createAppAPI內部我們肯定還需要元素掛載實現(xiàn)的render方法。
我們可以看到官網中的createRenderer API,中對外暴露了 render 和 createApp兩種方法,其實runtime-core中內部也是這樣實現(xiàn)的。

當然我們只是先將空架子搭建起來,后面還需要一步步填充功能。
createApp 內部實現(xiàn)
把上面例子來出來:
let app = createApp(App, { title: 'dy' }) // 組件 組件參數(shù)
app.mount('#app')
繼續(xù)回到createApp這個方法:
createRenderer實現(xiàn)一個渲染器,我們通過 createRenderer 可以解構其中對外暴露的方法,在 createApp 內部進行初始化,最后將其返回,用戶再通過mount對元素進行掛載。
const { createApp } = createRenderer(rendererOptions)
let app = createApp(component, rootProps) // 渲染器完成 將其返回
在初始化的時候我們通過用戶傳入的#app,進行初始化。在執(zhí)行用戶mount前,我們在createApp中存在實際元素掛載功能的mount,我們就需要先將其保存下來,在執(zhí)行用戶mount時,在其內部進行實際的元素掛載(即app.mount內部執(zhí)行我們實際mount功能)。
createApp內部:
let { mount } = app // 自己的
app.mount = (containerOrSelector) => { // 用戶傳入的 #app
const container = nodeOps.querySelector(containerOrSelector)
if (!container) return
container.innerHTML = ''
mount(container)
}
在掛載的時候,我們需要清空當前節(jié)點的內部元素,再將其掛載。
完整的createApp:
export const createApp = (component, rootProps = null) => {
const { createApp } = createRenderer(rendererOptions) // 將core中的createApp解構出來
// 創(chuàng)建一個渲染器 把功能傳遞給runtime-core
let app = createApp(component, rootProps)
let { mount } = app // 調用的是core中的mount
// app.mount 用戶初始化的mount ==> app.mount("#app");
// 在用戶的app.mount中再去執(zhí)行core中的mount ==> core中就會存在domAPI 需要渲染的組件 目標屬性,最后再去掛載到容器中
/**
* containerOrSelector 用戶傳入的容器
*/
app.mount = (containerOrSelector) => { // 用戶傳入的 #app
const container = nodeOps.querySelector(containerOrSelector)
if (!container) return // 不為空直接返回
// 在掛載之前清空
container.innerHTML = ''
// 調用的是core中的mount 處理節(jié)點后將container傳入mount
mount(container)
}
return app
}
這樣我們 createRenderer 就可以拿到節(jié)點操作的DOM API(rendererOptions)、需要渲染的組件(component)、目標屬性(rootProps)和需要掛載的位置-容器(container)。createRenderer也就可以獨立出來,放到runtime-core中,在core中我們單獨去實現(xiàn)元素的渲染。
runtime-core
視角轉到我們core中,在core中我們需要實現(xiàn)一個渲染器。目錄結構如下:

我們主要實現(xiàn)createRenderer這個功能對外暴露的兩個API,可以先來看看createApp中的 createAppAPI 。

createAppAPI
在 createAppAPI 中,我們目前需要實現(xiàn)真實元素的掛載。
說的通俗一點:掛載的核心就是根據(jù)傳入的組件對象,創(chuàng)造一個組件的虛擬節(jié)點,再將這個虛擬節(jié)點渲染到容器中。
具體分兩步走:
mount(container) {
// 1:創(chuàng)建組件虛擬節(jié)點
const vnode = cerateVNode(rootComponent, rootProps)// h函數(shù)
// 2:虛擬節(jié)點渲染到容器中
render(vnode, container)
}
cerateVNode 創(chuàng)建組件虛擬節(jié)點
我們首先要知道,虛擬節(jié)點是一個數(shù)據(jù)對象,用一個數(shù)據(jù)結構來對元素進行描述,當前元素是組件還是元素、它的字節(jié)點、攜帶的參數(shù)等有哪些。
類型表示
我們需要判斷傳過來的type是組件還是元素,改如何表示呢???????
在源碼中,通過ShapeFlags這個枚舉類就已經將類型描述的非常清楚了。我們可以查看源碼中的packages/shared/src/shapeFlags.ts這個文件:??????
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 // 組件
}
大部分類型我都進行了注釋。
有人可能不太理解1 << 1這種表達方式,這種是二進制的表示方法,1 << 1表示1向左移動一位,實際值就是1,同樣,那么1 << 2實際值就是4,依此類推。

這種寫法的好處就是,我們需要判斷一個值它是什么類型時,可以直接與對應的類型值相與。
舉個例子:
問:如何判斷當前值011(3) 既是元素又是函數(shù)式組件?
理解:用當前值011分別與ShapeFlags.ELEMENT、ShapeFlags.FUNCTIONAL_COMPONENT相與,如果不等于0就是包含。
答:

3與元素、函數(shù)式組件相與均不為0,所以011即時元素也是函數(shù)式組件。
同樣,如果想表示該類型即時元素也是函數(shù)式組件的話,我們可以將兩者類型進行組合(即相或)。
ShapeFlags.ELEMENT | ShapeFlags.FUNCTIONAL_COMPONENT就表示既是元素也是函數(shù)式組件。
虛擬節(jié)點創(chuàng)建
我們調用傳入的參數(shù)類型這種h函數(shù)接收的參數(shù):節(jié)點類型type、參數(shù)props、子節(jié)點children。
h('h2', { onClick: this.add, title: proxy.title }, 'hello dy' )
首先,我們需要判斷傳入過來的type是元素還是組件。我們這邊就先判斷是否是對象,如果是對象,那么節(jié)點類型就是組件,如果是字符串,那么該節(jié)點就是一個元素。
isObject(type) ? ShapeFlags.COMPONENT : isString(type) ? ShapeFlags.ELEMENT : 0
通過類型判斷,我們就可以拿到該節(jié)點的類型,除此之外,我們還需要它的參數(shù)、子節(jié)點、組件、真實節(jié)點等,并需要加上是否是虛擬節(jié)點的標志。
之所以會說runtime-core中是與平臺無關的運行時,是因為不管是瀏覽器、測試環(huán)境,都會先將其轉成可描述的虛擬節(jié)點vnode,再去進行節(jié)點對比、渲染等操作。
虛擬節(jié)點的創(chuàng)建功能:
export function cerateVNode(type, props, children = null) {
// type如果是字符串 元素 ; type是對象或者函數(shù) == 組件
const shapeFlag = isObject(type) ? ShapeFlags.COMPONENT : isString(type) ? ShapeFlags.ELEMENT : 0
// 瀏覽器或者測試環(huán)境都會轉成vnode
const vnode = {
__v_isVNode: true, // 是否是虛擬節(jié)點
type, // 節(jié)點類型
props,
children,
shapeFlag,
key: props && props.key, // patch對比需要
component: null,// 如果是組件的虛擬節(jié)點 需要保存組件的實例
el: null//真實節(jié)點
}
if (children) {
// 告訴節(jié)點 是什么樣的子節(jié)點
// 稍后渲染虛擬節(jié)點的時候,可以判斷兒子是數(shù)組就循環(huán)渲染
vnode.shapeFlag = vnode.shapeFlag | (isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN)
}
// vnode 描述出來當前是個什么節(jié)點,包含的子節(jié)點的類型是什么
return vnode
}
這樣我們創(chuàng)建出來的 vnode 就可以描述當前的節(jié)點類型了。
render 虛擬節(jié)點渲染到容器中
我們拿到虛擬節(jié)點后,接下來要做的就是將其渲染到容器中,即 render 的實現(xiàn)。
與之前vue類似,通過patch來實現(xiàn)節(jié)點的渲染和更新。我們這次主要實現(xiàn)的是節(jié)點的初始化,所以patch傳入的老的節(jié)點應該為null。
/** * 老的虛擬節(jié)點 * 新的虛擬節(jié)點 * 容器 */ patch(null, vnode, container)
通過傳入的新老vnode和當前的容器,我們需要對新老虛擬節(jié)點進行對比,然后判斷新節(jié)點的類型,進行不同類型的節(jié)點創(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ù)中判斷,如果沒有老虛擬節(jié)點則需要進行組件初始化操作,否則我們就需要進行組件的更新操作。
/**
*
* @param initialVNode 組件的虛擬節(jié)點
* @param container 容器
*/
const mountComponent = (initialVNode, container) => {
// 組件的掛載
console.log(initialVNode, container);
}
我們可以在組件初始化渲染中看看我們是否拿到上述案例中用戶傳入的虛擬節(jié)點和容器:

可以看到我們成功獲取到了當前的虛擬節(jié)點,此時的type就是用戶傳入的對象props、setup、render;shapeFlag是6,是因為當前節(jié)點是組件(函數(shù)式組件和普通組件的組合-ShapeFlags.COMPONENT)。
元素的掛載也是同樣的判斷流程:

我們也可以看到源碼中,不僅僅是對元素和組件情況,完整的vue框架還需要考慮節(jié)點類型是文本、注釋、靜態(tài)節(jié)點、代碼片段等等情況。

獲取到所需參數(shù)后,接下來的工作就是創(chuàng)建真實節(jié)點。將節(jié)點渲染到容器中,就需要用到我們之前實現(xiàn)的節(jié)點操作的API-rendererOptions。
// createRenderer函數(shù)
const {
insert: hostInsert, // 插入節(jié)點
remove: hostRemove, // 刪除節(jié)點
patchProp: hostPatchProp,
createElement: hostCreateElement, // 創(chuàng)建元素
createText: hostCreateText, // 創(chuàng)建文本
createComment: hostCreateComment, // 注釋
setText: hostSetText, // 設置文本中的內容
setElementText: hostSetElementText, // 設置文本內容
parentNode: hostParentNode, // 獲取父節(jié)點
nextSibling: hostNextSibling, // 兄弟節(jié)點
setScopeId: hostSetScopeId = NOOP, // 設置scope id
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = rendererOptions
將rendererOptions中的節(jié)點操作API結構出來,在組件、元素的掛載和更新函數(shù)中,根據(jù)不同情況創(chuàng)建元素節(jié)點、插入節(jié)點等DOM操作。
接下來的工作就是根據(jù)組件的虛擬節(jié)點創(chuàng)建一個真實節(jié)點,渲染到容器中。在此主要分為兩步驟:
1、給組件創(chuàng)建一個組件的實例;
2、需要給組件的實例進行賦值操作。然后render渲染到頁面上
以上就是vue開發(fā)runtime core中的虛擬節(jié)點示例詳解的詳細內容,更多關于vue runtime core虛擬節(jié)點的資料請關注腳本之家其它相關文章!
相關文章
如何在Vue單頁面中進行業(yè)務數(shù)據(jù)的上報
為什么要在標題里加上一個業(yè)務數(shù)據(jù)的上報呢,因為在咱們前端項目中,可上報的數(shù)據(jù)維度太多,比如還有性能數(shù)據(jù)、頁面錯誤數(shù)據(jù)、console捕獲等。這里我們只講解業(yè)務數(shù)據(jù)的埋點。2021-05-05
vue-quill-editor富文本編輯器上傳視頻功能詳解
需求需要實現(xiàn)富文本的功能,同時富文本中還可以上傳視頻和圖片,選來選去最后決定了用這個富文本編輯器,下面這篇文章主要給大家介紹了關于vue-quill-editor富文本編輯器上傳視頻功能的相關資料,需要的朋友可以參考下2023-05-05
vue中el-tab如何點擊不同標簽觸發(fā)不同函數(shù)的實現(xiàn)
el-tab本身的功能是點擊之后切換不同頁,本文主要介紹了vue中el-tab如何點擊不同標簽觸發(fā)不同函數(shù)的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2024-03-03
this.$router.push攜帶參數(shù)跳轉頁面的實現(xiàn)代碼
這篇文章主要介紹了this.$router.push攜帶參數(shù)跳轉頁面,this.$router.push進行頁面跳轉時,攜帶參數(shù)有params和query兩種方式,本文結合實例代碼給大家詳細講解,需要的朋友可以參考下2023-04-04

