詳解Vue3的虛擬DOM是如何生成的
h 函數(shù)
在官網(wǎng)上可以看到對(duì)h
函數(shù)的介紹和函數(shù)簽名;
可以先去看看官網(wǎng)的介紹,然后再來(lái)看看源碼的實(shí)現(xiàn),傳送門:
h 函數(shù)的實(shí)現(xiàn)
還是跟著我們之前的節(jié)奏,可以直接在h函數(shù)
調(diào)用上面打上斷點(diǎn),然后開(kāi)始調(diào)試進(jìn)入源碼:
const {h} = Vue; debugger; h('div');
直接就這樣進(jìn)入了h
函數(shù)的實(shí)現(xiàn),我們來(lái)看看h
函數(shù)的實(shí)現(xiàn):
function h(type, propsOrChildren, children) { // 通過(guò)參數(shù)數(shù)量來(lái)進(jìn)行重載 const l = arguments.length; // 如果參數(shù)數(shù)量為2,那么就有兩種情況 if (l === 2) { // 如果第二個(gè)參數(shù)是對(duì)象,并且不是數(shù)組 if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // 如果第二個(gè)參數(shù)是虛擬dom,那么就將第二個(gè)參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理 if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]); } // 如果第二個(gè)參數(shù)是對(duì)象,那么就將第二個(gè)參數(shù)作為props進(jìn)行處理 return createVNode(type, propsOrChildren); } else { // 如果第二個(gè)參數(shù)是數(shù)組,那么就將第二個(gè)參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理 return createVNode(type, null, propsOrChildren); } } else { // 如果參數(shù)數(shù)量不是2 if (l > 3) { // 并且參數(shù)數(shù)量大于3,那么就將第三個(gè)參數(shù)以及后面的參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理 children = Array.prototype.slice.call(arguments, 2); } else if (l === 3 && isVNode(children)) { // 如果參數(shù)數(shù)量等于3,并且第三個(gè)參數(shù)是虛擬dom,那么就將第三個(gè)參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理 children = [children]; } // 最后將第二個(gè)參數(shù)作為props,其余的參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理 return createVNode(type, propsOrChildren, children); } }
h
函數(shù)就是一個(gè)重載函數(shù),根據(jù)參數(shù)的不同,會(huì)有不同的處理邏輯,其實(shí)沒(méi)有什么好看的;
它最后將所有的參數(shù)都傳遞給了createVNode
函數(shù),也就是核心是createVNode
函數(shù);
createVNode 函數(shù)
由于我們上面的示例代碼中,只傳入了一個(gè)參數(shù),所以會(huì)跳過(guò)很多邏輯,簡(jiǎn)化后的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; // 如果是一個(gè)組件,并且還被設(shè)置成響應(yīng)式的了,則會(huì)提示并解包 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)魅肓艘粋€(gè)字符串的div
,所以shapeFlag
的值為1
;
這里的shapeFlag
其實(shí)是一個(gè)二進(jìn)制的值,它的值是由type
的類型來(lái)決定的,在ts
的源碼中有他們的定義:
// packages\shared\src\shapeFlags.ts export const enum ShapeFlags { ELEMENT = 1, // 普通dom元素 二進(jìn)制:0000 0001 十進(jìn)制:1 FUNCTIONAL_COMPONENT = 1 << 1, // 函數(shù)組件 二進(jìn)制:0000 0010 十進(jìn)制:2 STATEFUL_COMPONENT = 1 << 2, // 有狀態(tài)組件 二進(jìn)制:0000 0100 十進(jìn)制:4 TEXT_CHILDREN = 1 << 3, // 文本子節(jié)點(diǎn) 二進(jìn)制:0000 1000 十進(jìn)制:8 ARRAY_CHILDREN = 1 << 4, // 數(shù)組子節(jié)點(diǎn) 二進(jìn)制:0001 0000 十進(jìn)制:16 SLOTS_CHILDREN = 1 << 5, // 插槽 二進(jìn)制:0010 0000 十進(jìn)制:32 TELEPORT = 1 << 6, // TELEPORT組件 二進(jìn)制:0100 0000 十進(jìn)制:64 SUSPENSE = 1 << 7, // SUSPENSE組件 二進(jìn)制:1000 0000 十進(jìn)制:128 COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 沒(méi)弄清 二進(jìn)制:0001 0000 0000 十進(jìn)制:256 COMPONENT_KEPT_ALIVE = 1 << 9, // 沒(méi)弄清 二進(jìn)制:0010 0000 0000 十進(jìn)制:512 COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 普通組件,應(yīng)該是有狀態(tài)組件和函數(shù)組件的并集 }
這里我們可以驗(yàn)證一下這些值,寫(xiě)個(gè)demo
來(lái)看看:
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é)點(diǎn) const textChildren = h('div', 'text'); console.log('TEXT_CHILDREN', textChildren.shapeFlag); // 數(shù)組子節(jié)點(diǎn) const arrayChildren = h('div', [h('span'), h('span')]); console.log('ARRAY_CHILDREN', arrayChildren.shapeFlag); // 插槽子節(jié)點(diǎn) 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);
可以看到的是驗(yàn)證結(jié)果和我們上面的定義是一致的:
這里的文本子節(jié)點(diǎn)和數(shù)組子節(jié)點(diǎn)的值是9
和17
,這里的值是由shapeFlag
的值和TEXT_CHILDREN
和ARRAY_CHILDREN
的值進(jìn)行或運(yùn)算得到,這就要進(jìn)入到createBaseVNode
函數(shù)中去看看了。
createBaseVNode 函數(shù)
這里的createBaseVNode
函數(shù)就是定義了VNode
的一些屬性,我們拿文本子節(jié)點(diǎn)來(lái)做示例看看運(yùn)行邏輯(刪除不會(huì)執(zhí)行的邏輯的簡(jiǎn)化版代碼):
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é)點(diǎn)固定走這個(gè)分支 if (needFullChildrenNormalization) { // 使用 normalizeChildren 處理 children normalizeChildren(vnode, children); } // 最后返回 vnode return vnode; }
這里的代碼并不復(fù)雜,就是定義了vnode
,然后對(duì)children
進(jìn)行了處理,最后返回了vnode
;
我們當(dāng)前測(cè)試的文本子節(jié)點(diǎn),shapeFlag
的值為9
,這里就是通過(guò)normalizeChildren
函數(shù)來(lái)處理的,我們來(lái)看看normalizeChildren
函數(shù)的實(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 { // 走到這里,說(shuō)明 children 需要被規(guī)范為文本節(jié)點(diǎn) // 直接轉(zhuǎn)為字符串 children = String(children); // 如果是 teleport ,子節(jié)點(diǎn)會(huì)被標(biāo)記為 16,也就是數(shù)組節(jié)點(diǎn) if (shapeFlag & 64) { type = 16; // 這里會(huì)將 children 轉(zhuǎn)為數(shù)組 children = [createTextVNode(children)]; } else { // 如果是普通節(jié)點(diǎn),直接標(biāo)記為文本節(jié)點(diǎn),也就是 8 type = 8; } } // 最后將 children 賦值給 vnode.children vnode.children = children; // 然后將 type 的值進(jìn)行或運(yùn)算,賦值給 vnode.shapeFlag vnode.shapeFlag |= type; }
可以看到這里寫(xiě)了一堆條件分支,來(lái)判斷不同的子節(jié)點(diǎn)類型,最后將children
賦值給vnode.children
,然后將type
的值進(jìn)行或運(yùn)算,賦值給vnode.shapeFlag
;
或運(yùn)算
會(huì)得到什么結(jié)果呢?其實(shí)我們完全可以自己嘗試一下:
1 | 8
的結(jié)果是9
,這里的1
就是ELEMENT
,8
就是TEXT_CHILDREN
,所以最后的結(jié)果就是ELEMENT | TEXT_CHILDREN
,也就是9
;
位運(yùn)算
這樣做有什么意義呢?其實(shí)閱讀了這么長(zhǎng)時(shí)間的源碼,不難發(fā)現(xiàn)經(jīng)常會(huì)出現(xiàn)這樣的代碼:
if (shapeFlag & 8) { // ... }
這里就是一個(gè)位運(yùn)算
,這樣寫(xiě)無(wú)疑是增加了閱讀的難度,但是對(duì)代碼的性能以及一些邏輯上的判斷是有幫助的;
還是我們剛才的例子,我們來(lái)看看ELEMENT
和TEXT_CHILDREN
合并的值是9
,ELEMENT
和ARRAY_CHILDREN
合并的值是17
;
我們對(duì)它進(jìn)行一個(gè)位運(yùn)算
,看看結(jié)果是什么:
ELEMENT
和TEXT_CHILDREN
合并的值,與所以類型進(jìn)行與運(yùn)算
,結(jié)果如下:
ELEMENT
和ARRAY_CHILDREN
合并的值,與所有類型進(jìn)行與運(yùn)算
,結(jié)果如下:
可以看到合并后的值,只會(huì)與參與合并的值
進(jìn)行與運(yùn)算
得到的結(jié)果是參與合并的值
,這樣就可以通過(guò)與運(yùn)算
來(lái)判斷shapeFlag
的值是否包含某個(gè)類型;
而將這個(gè)過(guò)程進(jìn)行二進(jìn)制
來(lái)描述,就是這樣的:
# 這是 ELEMENT 和 TEXT_CHILDREN 合并的值 0000 1001 # 這是 ELEMENT 的值 0000 0001 # 進(jìn)行與運(yùn)算 0000 1001 &&&& &&&& 0000 0001 = = = = = 0000 0001
通過(guò)上面的例子,其實(shí)與運(yùn)算
就是將兩個(gè)值的二進(jìn)制中的相同位置的值進(jìn)行比較,如果都是1
,那么結(jié)果就是1
,否則就是0
;
而Vue
將每個(gè)節(jié)點(diǎn)的類型都定義成了2的n次方
,這樣就可以避免會(huì)出現(xiàn)相同位置的1
,這樣在進(jìn)行或運(yùn)算
的時(shí)候,就可以將所有的類型進(jìn)行合并,從而產(chǎn)生一個(gè)新的值;
如果是相同類型的節(jié)點(diǎn),那么shapeFlag
的值就是相同的,在進(jìn)行或運(yùn)算
的時(shí)候會(huì)得到相同的值,新值和原來(lái)的值是相同的,因?yàn)楸旧砭桶诉@個(gè)類型;
這樣新值就會(huì)包含所有參與合并的值的類型,就可以通過(guò)與運(yùn)算
來(lái)判斷shapeFlag
的值是否包含某個(gè)類型,設(shè)計(jì)非常的巧妙;
總結(jié)
這一篇主要學(xué)習(xí)了vnode
的擦創(chuàng)建過(guò)程,其實(shí)一個(gè)vnode
就是一個(gè)js對(duì)象
,本身并沒(méi)有什么特殊的;
特殊的是這個(gè)vnode
自帶的屬性,例如這一章詳細(xì)介紹的sahpeFlag
,這個(gè)屬性就是通過(guò)位運(yùn)算
來(lái)進(jìn)行合并的,這樣就可以通過(guò)與運(yùn)算
來(lái)判斷shapeFlag
的值是否包含某個(gè)類型;
而一個(gè)vnode
中并不是只有一個(gè)shapeFlag
屬性,還有很多其他的屬性,例如我們傳入的props
、children
、slot
等等;
這些屬性在Vue
的整個(gè)系統(tǒng)中又是如何使用的呢?這些將會(huì)在我們繼續(xù)深入源碼的過(guò)程中一一揭曉;
以上就是詳解Vue3的虛擬DOM是如何生成的的詳細(xì)內(nèi)容,更多關(guān)于Vue3虛擬DOM生成的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue移動(dòng)端html5頁(yè)面根據(jù)屏幕適配的四種解決方法
在vue移動(dòng)端h5頁(yè)面當(dāng)中,其中適配是經(jīng)常會(huì)遇到的問(wèn)題,這塊主要有四個(gè)方法可以適用。這篇文章主要介紹了vue移動(dòng)端h5頁(yè)面根據(jù)屏幕適配的四種方案 ,需要的朋友可以參考下2018-10-10使用vue實(shí)現(xiàn)一個(gè)電子簽名組件的示例代碼
這篇文章主要介紹了使用vue實(shí)現(xiàn)一個(gè)電子簽名組件的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01uniapp使用scroll-view下拉刷新無(wú)法取消的坑及解決
這篇文章主要介紹了uniapp使用scroll-view下拉刷新無(wú)法取消的坑及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05vue項(xiàng)目實(shí)現(xiàn)面包屑導(dǎo)航
這篇文章主要為大家詳細(xì)介紹了vue項(xiàng)目實(shí)現(xiàn)面包屑導(dǎo)航,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Vue彈窗Dialog最佳使用方案實(shí)戰(zhàn)
這篇文章主要為大家介紹了極度舒適的Vue彈窗Dialog最佳使用方案實(shí)戰(zhàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11