詳解Vue3的虛擬DOM是如何生成的
h 函數(shù)
在官網(wǎng)上可以看到對h函數(shù)的介紹和函數(shù)簽名;

可以先去看看官網(wǎng)的介紹,然后再來看看源碼的實(shí)現(xiàn),傳送門:
h 函數(shù)的實(shí)現(xiàn)
還是跟著我們之前的節(jié)奏,可以直接在h函數(shù)調(diào)用上面打上斷點(diǎn),然后開始調(diào)試進(jìn)入源碼:
const {h} = Vue;
debugger;
h('div');直接就這樣進(jìn)入了h函數(shù)的實(shí)現(xiàn),我們來看看h函數(shù)的實(shí)現(xiàn):
function h(type, propsOrChildren, children) {
// 通過參數(shù)數(shù)量來進(jìn)行重載
const l = arguments.length;
// 如果參數(shù)數(shù)量為2,那么就有兩種情況
if (l === 2) {
// 如果第二個參數(shù)是對象,并且不是數(shù)組
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// 如果第二個參數(shù)是虛擬dom,那么就將第二個參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren]);
}
// 如果第二個參數(shù)是對象,那么就將第二個參數(shù)作為props進(jìn)行處理
return createVNode(type, propsOrChildren);
} else {
// 如果第二個參數(shù)是數(shù)組,那么就將第二個參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理
return createVNode(type, null, propsOrChildren);
}
} else {
// 如果參數(shù)數(shù)量不是2
if (l > 3) {
// 并且參數(shù)數(shù)量大于3,那么就將第三個參數(shù)以及后面的參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理
children = Array.prototype.slice.call(arguments, 2);
} else if (l === 3 && isVNode(children)) {
// 如果參數(shù)數(shù)量等于3,并且第三個參數(shù)是虛擬dom,那么就將第三個參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理
children = [children];
}
// 最后將第二個參數(shù)作為props,其余的參數(shù)作為子節(jié)點(diǎn)進(jìn)行處理
return createVNode(type, propsOrChildren, children);
}
}h函數(shù)就是一個重載函數(shù),根據(jù)參數(shù)的不同,會有不同的處理邏輯,其實(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;
// 如果是一個組件,并且還被設(shè)置成響應(yīng)式的了,則會提示并解包
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其實(shí)是一個二進(jìn)制的值,它的值是由type的類型來決定的,在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, // 沒弄清 二進(jìn)制:0001 0000 0000 十進(jìn)制:256
COMPONENT_KEPT_ALIVE = 1 << 9, // 沒弄清 二進(jìn)制:0010 0000 0000 十進(jìn)制:512
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 普通組件,應(yīng)該是有狀態(tài)組件和函數(shù)組件的并集
}這里我們可以驗(yàn)證一下這些值,寫個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é)點(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)來做示例看看運(yùn)行邏輯(刪除不會執(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é)點(diǎn)固定走這個分支
if (needFullChildrenNormalization) {
// 使用 normalizeChildren 處理 children
normalizeChildren(vnode, children);
}
// 最后返回 vnode
return vnode;
}這里的代碼并不復(fù)雜,就是定義了vnode,然后對children進(jìn)行了處理,最后返回了vnode;
我們當(dāng)前測試的文本子節(jié)點(diǎn),shapeFlag的值為9,這里就是通過normalizeChildren函數(shù)來處理的,我們來看看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 {
// 走到這里,說明 children 需要被規(guī)范為文本節(jié)點(diǎn)
// 直接轉(zhuǎn)為字符串
children = String(children);
// 如果是 teleport ,子節(jié)點(diǎn)會被標(biāo)記為 16,也就是數(shù)組節(jié)點(diǎn)
if (shapeFlag & 64) {
type = 16;
// 這里會將 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;
}可以看到這里寫了一堆條件分支,來判斷不同的子節(jié)點(diǎn)類型,最后將children賦值給vnode.children,然后將type的值進(jìn)行或運(yùn)算,賦值給vnode.shapeFlag;
或運(yùn)算會得到什么結(jié)果呢?其實(shí)我們完全可以自己嘗試一下:

1 | 8 的結(jié)果是9,這里的1就是ELEMENT,8就是TEXT_CHILDREN,所以最后的結(jié)果就是ELEMENT | TEXT_CHILDREN,也就是9;
位運(yùn)算
這樣做有什么意義呢?其實(shí)閱讀了這么長時(shí)間的源碼,不難發(fā)現(xiàn)經(jīng)常會出現(xiàn)這樣的代碼:
if (shapeFlag & 8) {
// ...
}這里就是一個位運(yùn)算,這樣寫無疑是增加了閱讀的難度,但是對代碼的性能以及一些邏輯上的判斷是有幫助的;
還是我們剛才的例子,我們來看看ELEMENT和TEXT_CHILDREN合并的值是9,ELEMENT和ARRAY_CHILDREN合并的值是17;
我們對它進(jìn)行一個位運(yùn)算,看看結(jié)果是什么:
ELEMENT和TEXT_CHILDREN合并的值,與所以類型進(jìn)行與運(yùn)算,結(jié)果如下:

ELEMENT和ARRAY_CHILDREN合并的值,與所有類型進(jìn)行與運(yùn)算,結(jié)果如下:

可以看到合并后的值,只會與參與合并的值進(jìn)行與運(yùn)算得到的結(jié)果是參與合并的值,這樣就可以通過與運(yùn)算來判斷shapeFlag的值是否包含某個類型;
而將這個過程進(jìn)行二進(jìn)制來描述,就是這樣的:
# 這是 ELEMENT 和 TEXT_CHILDREN 合并的值 0000 1001 # 這是 ELEMENT 的值 0000 0001 # 進(jìn)行與運(yùn)算 0000 1001 &&&& &&&& 0000 0001 = = = = = 0000 0001
通過上面的例子,其實(shí)與運(yùn)算就是將兩個值的二進(jìn)制中的相同位置的值進(jìn)行比較,如果都是1,那么結(jié)果就是1,否則就是0;
而Vue將每個節(jié)點(diǎn)的類型都定義成了2的n次方,這樣就可以避免會出現(xiàn)相同位置的1,這樣在進(jìn)行或運(yùn)算的時(shí)候,就可以將所有的類型進(jìn)行合并,從而產(chǎn)生一個新的值;
如果是相同類型的節(jié)點(diǎn),那么shapeFlag的值就是相同的,在進(jìn)行或運(yùn)算的時(shí)候會得到相同的值,新值和原來的值是相同的,因?yàn)楸旧砭桶诉@個類型;
這樣新值就會包含所有參與合并的值的類型,就可以通過與運(yùn)算來判斷shapeFlag的值是否包含某個類型,設(shè)計(jì)非常的巧妙;
總結(jié)
這一篇主要學(xué)習(xí)了vnode的擦創(chuàng)建過程,其實(shí)一個vnode就是一個js對象,本身并沒有什么特殊的;
特殊的是這個vnode自帶的屬性,例如這一章詳細(xì)介紹的sahpeFlag,這個屬性就是通過位運(yùn)算來進(jìn)行合并的,這樣就可以通過與運(yùn)算來判斷shapeFlag的值是否包含某個類型;
而一個vnode中并不是只有一個shapeFlag屬性,還有很多其他的屬性,例如我們傳入的props、children、slot等等;
這些屬性在Vue的整個系統(tǒng)中又是如何使用的呢?這些將會在我們繼續(xù)深入源碼的過程中一一揭曉;
以上就是詳解Vue3的虛擬DOM是如何生成的的詳細(xì)內(nèi)容,更多關(guān)于Vue3虛擬DOM生成的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue移動端html5頁面根據(jù)屏幕適配的四種解決方法
在vue移動端h5頁面當(dāng)中,其中適配是經(jīng)常會遇到的問題,這塊主要有四個方法可以適用。這篇文章主要介紹了vue移動端h5頁面根據(jù)屏幕適配的四種方案 ,需要的朋友可以參考下2018-10-10
使用vue實(shí)現(xiàn)一個電子簽名組件的示例代碼
這篇文章主要介紹了使用vue實(shí)現(xiàn)一個電子簽名組件的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
uniapp使用scroll-view下拉刷新無法取消的坑及解決
這篇文章主要介紹了uniapp使用scroll-view下拉刷新無法取消的坑及解決,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
vue項(xiàng)目實(shí)現(xiàn)面包屑導(dǎo)航
這篇文章主要為大家詳細(xì)介紹了vue項(xiàng)目實(shí)現(xiàn)面包屑導(dǎo)航,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Vue彈窗Dialog最佳使用方案實(shí)戰(zhàn)
這篇文章主要為大家介紹了極度舒適的Vue彈窗Dialog最佳使用方案實(shí)戰(zhàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11

