詳解Vue3的虛擬DOM是如何生成的
h 函數(shù)
在官網(wǎng)上可以看到對h
函數(shù)的介紹和函數(shù)簽名;
可以先去看看官網(wǎng)的介紹,然后再來看看源碼的實現(xiàn),傳送門:
h 函數(shù)的實現(xiàn)
還是跟著我們之前的節(jié)奏,可以直接在h函數(shù)
調(diào)用上面打上斷點,然后開始調(diào)試進入源碼:
const {h} = Vue; debugger; h('div');
直接就這樣進入了h
函數(shù)的實現(xiàn),我們來看看h
函數(shù)的實現(xiàn):
function h(type, propsOrChildren, children) { // 通過參數(shù)數(shù)量來進行重載 const l = arguments.length; // 如果參數(shù)數(shù)量為2,那么就有兩種情況 if (l === 2) { // 如果第二個參數(shù)是對象,并且不是數(shù)組 if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // 如果第二個參數(shù)是虛擬dom,那么就將第二個參數(shù)作為子節(jié)點進行處理 if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]); } // 如果第二個參數(shù)是對象,那么就將第二個參數(shù)作為props進行處理 return createVNode(type, propsOrChildren); } else { // 如果第二個參數(shù)是數(shù)組,那么就將第二個參數(shù)作為子節(jié)點進行處理 return createVNode(type, null, propsOrChildren); } } else { // 如果參數(shù)數(shù)量不是2 if (l > 3) { // 并且參數(shù)數(shù)量大于3,那么就將第三個參數(shù)以及后面的參數(shù)作為子節(jié)點進行處理 children = Array.prototype.slice.call(arguments, 2); } else if (l === 3 && isVNode(children)) { // 如果參數(shù)數(shù)量等于3,并且第三個參數(shù)是虛擬dom,那么就將第三個參數(shù)作為子節(jié)點進行處理 children = [children]; } // 最后將第二個參數(shù)作為props,其余的參數(shù)作為子節(jié)點進行處理 return createVNode(type, propsOrChildren, children); } }
h
函數(shù)就是一個重載函數(shù),根據(jù)參數(shù)的不同,會有不同的處理邏輯,其實沒有什么好看的;
它最后將所有的參數(shù)都傳遞給了createVNode
函數(shù),也就是核心是createVNode
函數(shù);
createVNode 函數(shù)
由于我們上面的示例代碼中,只傳入了一個參數(shù),所以會跳過很多邏輯,簡化后的createVNode
函數(shù)如下:
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) { // 獲取 shapeFlag const shapeFlag = isString(type) ? 1 : isSuspense(type) ? 128 : isTeleport(type) ? 64 : isObject(type) ? 4 : isFunction(type) ? 2 : 0; // 如果是一個組件,并且還被設置成響應式的了,則會提示并解包 if (shapeFlag & 4 && isProxy(type)) { type = toRaw(type); warn( `Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with \`markRaw\` or using \`shallowRef\` instead of \`ref\`.`, ` Component that was made reactive: `, type ); } // 最后調(diào)用 createBaseVNode 創(chuàng)建 VNode return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ); }
這里主要是獲取了shapeFlag
,我們上面?zhèn)魅肓艘粋€字符串的div
,所以shapeFlag
的值為1
;
這里的shapeFlag
其實是一個二進制的值,它的值是由type
的類型來決定的,在ts
的源碼中有他們的定義:
// packages\shared\src\shapeFlags.ts export const enum ShapeFlags { ELEMENT = 1, // 普通dom元素 二進制:0000 0001 十進制:1 FUNCTIONAL_COMPONENT = 1 << 1, // 函數(shù)組件 二進制:0000 0010 十進制:2 STATEFUL_COMPONENT = 1 << 2, // 有狀態(tài)組件 二進制:0000 0100 十進制:4 TEXT_CHILDREN = 1 << 3, // 文本子節(jié)點 二進制:0000 1000 十進制:8 ARRAY_CHILDREN = 1 << 4, // 數(shù)組子節(jié)點 二進制:0001 0000 十進制:16 SLOTS_CHILDREN = 1 << 5, // 插槽 二進制:0010 0000 十進制:32 TELEPORT = 1 << 6, // TELEPORT組件 二進制:0100 0000 十進制:64 SUSPENSE = 1 << 7, // SUSPENSE組件 二進制:1000 0000 十進制:128 COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 沒弄清 二進制:0001 0000 0000 十進制:256 COMPONENT_KEPT_ALIVE = 1 << 9, // 沒弄清 二進制:0010 0000 0000 十進制:512 COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 普通組件,應該是有狀態(tài)組件和函數(shù)組件的并集 }
這里我們可以驗證一下這些值,寫個demo
來看看:
const {h} = Vue; // 普通元素 const element = h('div'); console.log('ELEMENT', element.shapeFlag); // 函數(shù)式組件 const functionalComponent = h(() => h('div')); console.log('FUNCTIONAL_COMPONENT', functionalComponent.shapeFlag); // 有狀態(tài)組件 const statefulComponent = h({ render() { return h('div'); } }); console.log('STATEFUL_COMPONENT', statefulComponent.shapeFlag); // 文本子節(jié)點 const textChildren = h('div', 'text'); console.log('TEXT_CHILDREN', textChildren.shapeFlag); // 數(shù)組子節(jié)點 const arrayChildren = h('div', [h('span'), h('span')]); console.log('ARRAY_CHILDREN', arrayChildren.shapeFlag); // 插槽子節(jié)點 const slotsChildren = h({ render() { return h('div', this.$slots.default()); } }, null, () => 'slotChildren'); console.log('SLOTS_CHILDREN', slotsChildren.shapeFlag); // teleport組件 const teleport = h(Vue.Teleport); console.log('TELEPORT', teleport.shapeFlag); // suspense組件 const suspense = h(Vue.Suspense); console.log('SUSPENSE', suspense.shapeFlag);
可以看到的是驗證結(jié)果和我們上面的定義是一致的:
這里的文本子節(jié)點和數(shù)組子節(jié)點的值是9
和17
,這里的值是由shapeFlag
的值和TEXT_CHILDREN
和ARRAY_CHILDREN
的值進行或運算得到,這就要進入到createBaseVNode
函數(shù)中去看看了。
createBaseVNode 函數(shù)
這里的createBaseVNode
函數(shù)就是定義了VNode
的一些屬性,我們拿文本子節(jié)點來做示例看看運行邏輯(刪除不會執(zhí)行的邏輯的簡化版代碼):
function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1, isBlockNode = false, needFullChildrenNormalization = false) { // 定義 vnode const vnode = { __v_isVNode: true, __v_skip: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null, children, component: null, suspense: null, ssContent: null, ssFallback: null, dirs: null, transition: null, el: null, anchor: null, target: null, targetAnchor: null, staticCount: 0, shapeFlag, patchFlag, dynamicProps, dynamicChildren: null, appContext: null, ctx: currentRenderingInstance }; // 普通節(jié)點固定走這個分支 if (needFullChildrenNormalization) { // 使用 normalizeChildren 處理 children normalizeChildren(vnode, children); } // 最后返回 vnode return vnode; }
這里的代碼并不復雜,就是定義了vnode
,然后對children
進行了處理,最后返回了vnode
;
我們當前測試的文本子節(jié)點,shapeFlag
的值為9
,這里就是通過normalizeChildren
函數(shù)來處理的,我們來看看normalizeChildren
函數(shù)的實現(xiàn):
function normalizeChildren(vnode, children) { let type = 0; const { shapeFlag } = vnode; if (children == null) { // ... } else if (isArray(children)) { // ... } else if (typeof children === "object") { // ... } else if (isFunction(children)) { // ... } else { // 走到這里,說明 children 需要被規(guī)范為文本節(jié)點 // 直接轉(zhuǎn)為字符串 children = String(children); // 如果是 teleport ,子節(jié)點會被標記為 16,也就是數(shù)組節(jié)點 if (shapeFlag & 64) { type = 16; // 這里會將 children 轉(zhuǎn)為數(shù)組 children = [createTextVNode(children)]; } else { // 如果是普通節(jié)點,直接標記為文本節(jié)點,也就是 8 type = 8; } } // 最后將 children 賦值給 vnode.children vnode.children = children; // 然后將 type 的值進行或運算,賦值給 vnode.shapeFlag vnode.shapeFlag |= type; }
可以看到這里寫了一堆條件分支,來判斷不同的子節(jié)點類型,最后將children
賦值給vnode.children
,然后將type
的值進行或運算,賦值給vnode.shapeFlag
;
或運算
會得到什么結(jié)果呢?其實我們完全可以自己嘗試一下:
1 | 8
的結(jié)果是9
,這里的1
就是ELEMENT
,8
就是TEXT_CHILDREN
,所以最后的結(jié)果就是ELEMENT | TEXT_CHILDREN
,也就是9
;
位運算
這樣做有什么意義呢?其實閱讀了這么長時間的源碼,不難發(fā)現(xiàn)經(jīng)常會出現(xiàn)這樣的代碼:
if (shapeFlag & 8) { // ... }
這里就是一個位運算
,這樣寫無疑是增加了閱讀的難度,但是對代碼的性能以及一些邏輯上的判斷是有幫助的;
還是我們剛才的例子,我們來看看ELEMENT
和TEXT_CHILDREN
合并的值是9
,ELEMENT
和ARRAY_CHILDREN
合并的值是17
;
我們對它進行一個位運算
,看看結(jié)果是什么:
ELEMENT
和TEXT_CHILDREN
合并的值,與所以類型進行與運算
,結(jié)果如下:
ELEMENT
和ARRAY_CHILDREN
合并的值,與所有類型進行與運算
,結(jié)果如下:
可以看到合并后的值,只會與參與合并的值
進行與運算
得到的結(jié)果是參與合并的值
,這樣就可以通過與運算
來判斷shapeFlag
的值是否包含某個類型;
而將這個過程進行二進制
來描述,就是這樣的:
# 這是 ELEMENT 和 TEXT_CHILDREN 合并的值 0000 1001 # 這是 ELEMENT 的值 0000 0001 # 進行與運算 0000 1001 &&&& &&&& 0000 0001 = = = = = 0000 0001
通過上面的例子,其實與運算
就是將兩個值的二進制中的相同位置的值進行比較,如果都是1
,那么結(jié)果就是1
,否則就是0
;
而Vue
將每個節(jié)點的類型都定義成了2的n次方
,這樣就可以避免會出現(xiàn)相同位置的1
,這樣在進行或運算
的時候,就可以將所有的類型進行合并,從而產(chǎn)生一個新的值;
如果是相同類型的節(jié)點,那么shapeFlag
的值就是相同的,在進行或運算
的時候會得到相同的值,新值和原來的值是相同的,因為本身就包含了這個類型;
這樣新值就會包含所有參與合并的值的類型,就可以通過與運算
來判斷shapeFlag
的值是否包含某個類型,設計非常的巧妙;
總結(jié)
這一篇主要學習了vnode
的擦創(chuàng)建過程,其實一個vnode
就是一個js對象
,本身并沒有什么特殊的;
特殊的是這個vnode
自帶的屬性,例如這一章詳細介紹的sahpeFlag
,這個屬性就是通過位運算
來進行合并的,這樣就可以通過與運算
來判斷shapeFlag
的值是否包含某個類型;
而一個vnode
中并不是只有一個shapeFlag
屬性,還有很多其他的屬性,例如我們傳入的props
、children
、slot
等等;
這些屬性在Vue
的整個系統(tǒng)中又是如何使用的呢?這些將會在我們繼續(xù)深入源碼的過程中一一揭曉;
以上就是詳解Vue3的虛擬DOM是如何生成的的詳細內(nèi)容,更多關于Vue3虛擬DOM生成的資料請關注腳本之家其它相關文章!
相關文章
vue移動端html5頁面根據(jù)屏幕適配的四種解決方法
在vue移動端h5頁面當中,其中適配是經(jīng)常會遇到的問題,這塊主要有四個方法可以適用。這篇文章主要介紹了vue移動端h5頁面根據(jù)屏幕適配的四種方案 ,需要的朋友可以參考下2018-10-10uniapp使用scroll-view下拉刷新無法取消的坑及解決
這篇文章主要介紹了uniapp使用scroll-view下拉刷新無法取消的坑及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05