深入聊聊 Vue.js 3.2 關(guān)于 vnode 部分的優(yōu)化
背景
上一篇文章,我分析了 Vue.js 3.2 關(guān)于響應(yīng)式部分的優(yōu)化,此外,在這次優(yōu)化升級中,還有一個關(guān)于運行時的優(yōu)化:
~200% faster creation of plain element VNodes
即針對普通元素類型 vnode 的創(chuàng)建,提升了約 200% 的性能。這也是一個非常偉大的優(yōu)化,是 Vue 的官方核心開發(fā)者 HcySunYang 實現(xiàn)的,可以參考這個 PR。
那么具體是怎么做的呢,在分析實現(xiàn)前,我想先帶你了解一些 vnode 的背景知識。
什么是 vnode
vnode 本質(zhì)上是用來描述 DOM 的 JavaScript 對象,它在 Vue.js 中可以描述不同類型的節(jié)點,比如普通元素節(jié)點、組件節(jié)點等。
普通元素 vnode
什么是普通元素節(jié)點呢?舉個例子,在 HTML 中我們使用 標(biāo)簽來寫一個按鈕:
<button?class="btn"?style="width:100px;height:50px">click?mebutton>
我們可以用 vnode 這樣表示 標(biāo)簽:
const?vnode?=?{
??type:?'button',
??props:?{?
????'class':?'btn',
????style:?{
??????width:?'100px',
??????height:?'50px'
????}
??},
??children:?'click?me'
}
其中,type 屬性表示 DOM 的標(biāo)簽類型;props 屬性表示 DOM 的一些附加信息,比如 style 、class 等;children 屬性表示 DOM 的子節(jié)點,在該示例中它是一個簡單的文本字符串,當(dāng)然,children 也可以是一個 vnode 數(shù)組。
組件 vnode
vnode 除了可以像上面那樣用于描述一個真實的 DOM,也可以用來描述組件。舉個例子,我們在模板中引入一個組件標(biāo)簽 :
<custom-component?msg="test">custom-component>
我們可以用 vnode 這樣表示 組件標(biāo)簽:
const?CustomComponent?=?{
??//?在這里定義組件對象
}
const?vnode?=?{
??type:?CustomComponent,
??props:?{?
????msg:?'test'
??}
}
組件 vnode 其實是對抽象事物的描述,這是因為我們并不會在頁面上真正渲染一個 標(biāo)簽,而最終會渲染組件內(nèi)部定義的 HTML 標(biāo)簽。
除了上述兩種 vnode 類型外,還有純文本 vnode、注釋 vnode 等等。
另外,Vue.js 3.x 內(nèi)部還針對 vnode 的 type,做了更詳盡的分類,包括 Suspense、Teleport 等,并且把 vnode 的類型信息做了編碼,以便在后面 vnode的掛載階段,可以根據(jù)不同的類型執(zhí)行相應(yīng)的處理邏輯:
//?runtime-core/src/vnode.ts
const?shapeFlag?=?isString(type)
????1?/*?ELEMENT?*/
??:?isSuspense(type)
??????128?/*?SUSPENSE?*/
????:?isTeleport(type)
????????64?/*?TELEPORT?*/
??????:?isObject(type)
??????????4?/*?STATEFUL_COMPONENT?*/
????????:?isFunction(type)
????????????2?/*?FUNCTIONAL_COMPONENT?*/
??????????:?0;
vnode 的優(yōu)勢
知道什么是 vnode 后,你可能會好奇,那么 vnode 有什么優(yōu)勢呢?為什么一定要設(shè)計 vnode 這樣的數(shù)據(jù)結(jié)構(gòu)呢?
首先是抽象,引入 vnode,可以把渲染過程抽象化,從而使得組件的抽象能力也得到提升。
其次是跨平臺,因為 patch vnode 的過程不同平臺可以有自己的實現(xiàn),基于 vnode 再做服務(wù)端渲染、weex 平臺、小程序平臺的渲染都變得容易了很多。
不過這里要特別注意,在瀏覽器端使用 vnode 并不意味著不用操作 DOM 了,很多人會誤以為 vnode 的性能一定比手動操作原生 DOM 好,這個其實是不一定的。
因為這種基于 vnode 實現(xiàn)的 MVVM 框架,在每次組件渲染生成 vnode 的過程中,會有一定的 JavaScript 耗時,尤其是是大組件。舉個例子,一個 1000 * 10 的 Table 組件,組件渲染生成 vnode 的過程會遍歷 1000 * 10 次去創(chuàng)建內(nèi)部 cell vnode,整個耗時就會變得比較長,再加上掛載 vnode 生成 DOM 的過程也會有一定的耗時,當(dāng)我們?nèi)ジ陆M件的時候,用戶會感覺到明顯的卡頓。
雖然 diff 算法在減少 DOM 操作方面足夠優(yōu)秀,但最終還是免不了操作 DOM,所以說性能并不是 vnode 的優(yōu)勢。
如何創(chuàng)建 vnode
通常我們開發(fā)組件都是編寫組件的模板,并不會手寫 vnode,那么 vnode 是如何創(chuàng)建的呢?
我們知道,組件模板經(jīng)過編譯,會生成對應(yīng)的 render 函數(shù),在 render 函數(shù)內(nèi)部,會執(zhí)行 createVNode 函數(shù)創(chuàng)建 vnode 對象,我們來看一下 Vue.js 3.2 之前它的實現(xiàn):
function?createVNode(type,?props?=?null,?children?=?null,?patchFlag?=?0,?dynamicProps?=?null,?isBlockNode?=?false)?{
??if?(!type?||?type?===?NULL_DYNAMIC_COMPONENT)?{
????if?((process.env.NODE_ENV?!==?'production')?&&?!type)?{
??????warn(`Invalid?vnode?type?when?creating?vnode:?${type}.`)
????}
????type?=?Comment
??}
??if?(isVNode(type))?{
????const?cloned?=?cloneVNode(type,?props,?true?/*?mergeRef:?true?*/)
????if?(children)?{
??????normalizeChildren(cloned,?children)
????}
????return?cloned
??}
??//?類組件的標(biāo)準(zhǔn)化
??if?(isClassComponent(type))?{
????type?=?type.__vccOpts
??}
??//?class?和?style?標(biāo)準(zhǔn)化.
??if?(props)?{
????if?(isProxy(props)?||?InternalObjectKey?in?props)?{
??????props?=?extend({},?props)
????}
????let?{?class:?klass,?style?}?=?props
????if?(klass?&&?!isString(klass))?{
??????props.class?=?normalizeClass(klass)
????}
????if?(isObject(style))?{
??????if?(isProxy(style)?&&?!isArray(style))?{
????????style?=?extend({},?style)
??????}
??????props.style?=?normalizeStyle(style)
????}
??}
??//?根據(jù)?vnode?的類型編碼
??const?shapeFlag?=?isString(type)
??????1?/*?ELEMENT?*/
????:?isSuspense(type)
????????128?/*?SUSPENSE?*/
??????:?isTeleport(type)
??????????64?/*?TELEPORT?*/
????????:?isObject(type)
????????????4?/*?STATEFUL_COMPONENT?*/
??????????:?isFunction(type)
??????????????2?/*?FUNCTIONAL_COMPONENT?*/
????????????:?0
??if?((process.env.NODE_ENV?!==?'production')?&&?shapeFlag?&?4?/*?STATEFUL_COMPONENT?*/?&&?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\`.`,?`\nComponent?that?was?made?reactive:?`,?type)
??}
??const?vnode?=?{
????__v_isVNode:?true,
????__v_skip:?true,
????type,
????props,
????key:?props?&&?normalizeKey(props),
????ref:?props?&&?normalizeRef(props),
????scopeId:?currentScopeId,
????slotScopeIds:?null,
????children:?null,
????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
??}
??if?((process.env.NODE_ENV?!==?'production')?&&?vnode.key?!==?vnode.key)?{
????warn(`VNode?created?with?invalid?key?(NaN).?VNode?type:`,?vnode.type)
??}
??normalizeChildren(vnode,?children)
??//?標(biāo)準(zhǔn)化?suspense?子節(jié)點
??if?(shapeFlag?&?128?/*?SUSPENSE?*/)?{
????type.normalize(vnode)
??}
??if?(isBlockTreeEnabled?>?0?&&
????!isBlockNode?&&
????currentBlock?&&
????(patchFlag?>?0?||?shapeFlag?&?6?/*?COMPONENT?*/)?&&
????patchFlag?!==?32?/*?HYDRATE_EVENTS?*/)?{
????currentBlock.push(vnode)
??}
??return?vnode
}
可以看到,創(chuàng)建 vnode 的過程做了很多事情,其中有很多判斷的邏輯,比如判斷 type 是否為空:
if?(!type?||?type?===?NULL_DYNAMIC_COMPONENT)?{
??if?((process.env.NODE_ENV?!==?'production')?&&?!type)?{
????warn(`Invalid?vnode?type?when?creating?vnode:?${type}.`)
??}
??type?=?Comment
}
判斷 type 是不是一個 vnode 節(jié)點:
if?(isVNode(type))?{
??const?cloned?=?cloneVNode(type,?props,?true?/*?mergeRef:?true?*/)
??if?(children)?{
????normalizeChildren(cloned,?children)
??}
??return?cloned
}
判斷 type 是不是一個 class 類型的組件:
if?(isClassComponent(type))?{
????type?=?type.__vccOpts
??}
除此之外,還會對屬性中的 style 和 class 執(zhí)行標(biāo)準(zhǔn)化,其中也會有一些判斷邏輯:
if?(props)?{
??if?(isProxy(props)?||?InternalObjectKey?in?props)?{
????props?=?extend({},?props)
??}
??let?{?class:?klass,?style?}?=?props
??if?(klass?&&?!isString(klass))?{
????props.class?=?normalizeClass(klass)
??}
??if?(isObject(style))?{
????if?(isProxy(style)?&&?!isArray(style))?{
??????style?=?extend({},?style)
????}
????props.style?=?normalizeStyle(style)
??}
}
接下來還會根據(jù) vnode 的類型編碼:
const?shapeFlag?=?isString(type)
????1?/*?ELEMENT?*/
??:?isSuspense(type)
??????128?/*?SUSPENSE?*/
????:?isTeleport(type)
????????64?/*?TELEPORT?*/
??????:?isObject(type)
??????????4?/*?STATEFUL_COMPONENT?*/
????????:?isFunction(type)
????????????2?/*?FUNCTIONAL_COMPONENT?*/
??????????:?0
然后就是創(chuàng)建 vnode 對象,創(chuàng)建完后還會執(zhí)行 normalizeChildren 去標(biāo)準(zhǔn)化子節(jié)點,這個過程也會有一系列的判斷邏輯。
創(chuàng)建 vnode 過程的優(yōu)化
仔細(xì)想想,vnode 本質(zhì)上就是一個 JavaScript 對象,之所以在創(chuàng)建過程中做很多判斷,是因為要處理各種各樣的情況。然而對于普通元素 vnode 而言,完全不需要這么多的判斷邏輯,因此對于普通元素 vnode,使用 createVNode 函數(shù)創(chuàng)建就是一種浪費。
順著這個思路,就可以在模板編譯階段,針對普通元素節(jié)點,使用新的函數(shù)來創(chuàng)建 vnode,Vue.js 3.2 就是這么做的,舉個例子:
<template>
??<div?class="home">
????<img?alt="Vue?logo"?src="../assets/logo.png">
????<HelloWorld?msg="Welcome?to?Your?Vue.js?App"/>
??div>
template>
借助于模板導(dǎo)出工具,可以看到它編譯后的 render 函數(shù):
import?{?createElementVNode?as?_createElementVNode,?resolveComponent?as?_resolveComponent,?createVNode?as?_createVNode,?openBlock?as?_openBlock,?createElementBlock?as?_createElementBlock?}?from?"vue"
const?_hoisted_1?=?{?class:?"home"?}
const?_hoisted_2?=?/*#__PURE__*/_createElementVNode("img",?{
??alt:?"Vue?logo",
??src:?"../assets/logo.png"
},?null,?-1?/*?HOISTED?*/)
export?function?render(_ctx,?_cache,?$props,?$setup,?$data,?$options)?{
??const?_component_HelloWorld?=?_resolveComponent("HelloWorld")
??return?(_openBlock(),?_createElementBlock("template",?null,?[
????_createElementVNode("div",?_hoisted_1,?[
??????_hoisted_2,
??????_createVNode(_component_HelloWorld,?{?msg:?"Welcome?to?Your?Vue.js?App"?})
????])
??]))
}
針對于 div 節(jié)點,這里使用了 createElementVNode 方法而并非 createVNode 方法,而 createElementVNode 在內(nèi)部是 createBaseVNode 的別名,來看它的實現(xiàn):
function?createBaseVNode(type,?props?=?null,?children?=?null,?patchFlag?=?0,?dynamicProps?=?null,?shapeFlag?=?type?===?Fragment???0?:?1?/*?ELEMENT?*/,?isBlockNode?=?false,?needFullChildrenNormalization?=?false)?{
??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
??}
??if?(needFullChildrenNormalization)?{
????normalizeChildren(vnode,?children)
????if?(shapeFlag?&?128?/*?SUSPENSE?*/)?{
??????type.normalize(vnode)
????}
??}
??else?if?(children)?{
????vnode.shapeFlag?|=?isString(children)
????????8?/*?TEXT_CHILDREN?*/
??????:?16?/*?ARRAY_CHILDREN?*/
??}
??if?((process.env.NODE_ENV?!==?'production')?&&?vnode.key?!==?vnode.key)?{
????warn(`VNode?created?with?invalid?key?(NaN).?VNode?type:`,?vnode.type)
??}
??if?(isBlockTreeEnabled?>?0?&&
????!isBlockNode?&&
????currentBlock?&&
????(vnode.patchFlag?>?0?||?shapeFlag?&?6?/*?COMPONENT?*/)?&&
????vnode.patchFlag?!==?32?/*?HYDRATE_EVENTS?*/)?{
????currentBlock.push(vnode)
??}
??return?vnode
}
可以看到,createBaseVNode 內(nèi)部僅僅是創(chuàng)建了 vnode 對象,然后做了一些 block 邏輯的處理。相比于之前的 createVNode 的實現(xiàn),createBaseVNode 少執(zhí)行了很多判斷邏輯,自然性能就獲得了提升。
而 createVNode 的實現(xiàn),是基于 createBaseVNode 做的一層封裝:
function?createVNode(type,?props?=?null,?children?=?null,?patchFlag?=?0,?dynamicProps?=?null,?isBlockNode?=?false)?{
??if?(!type?||?type?===?NULL_DYNAMIC_COMPONENT)?{
????if?((process.env.NODE_ENV?!==?'production')?&&?!type)?{
??????warn(`Invalid?vnode?type?when?creating?vnode:?${type}.`)
????}
????type?=?Comment$1
??}
??if?(isVNode(type))?{
????const?cloned?=?cloneVNode(type,?props,?true?/*?mergeRef:?true?*/)
????if?(children)?{
??????normalizeChildren(cloned,?children)
????}
????return?cloned
??}
??if?(isClassComponent(type))?{
????type?=?type.__vccOpts
??}
??if?(props)?{
????props?=?guardReactiveProps(props)
????let?{?class:?klass,?style?}?=?props
????if?(klass?&&?!isString(klass))?{
??????props.class?=?normalizeClass(klass)
????}
????if?(isObject$1(style))?{
??????if?(isProxy(style)?&&?!isArray(style))?{
????????style?=?extend({},?style)
??????}
??????props.style?=?normalizeStyle(style)
????}
??}
??const?shapeFlag?=?isString(type)
??????1?/*?ELEMENT?*/
????:?isSuspense(type)
????????128?/*?SUSPENSE?*/
??????:?isTeleport(type)
??????????64?/*?TELEPORT?*/
????????:?isObject$1(type)
????????????4?/*?STATEFUL_COMPONENT?*/
??????????:?isFunction$1(type)
??????????????2?/*?FUNCTIONAL_COMPONENT?*/
????????????:?0
??if?((process.env.NODE_ENV?!==?'production')?&&?shapeFlag?&?4?/*?STATEFUL_COMPONENT?*/?&&?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\`.`,?`\nComponent?that?was?made?reactive:?`,?type)
??}
??return?createBaseVNode(type,?props,?children,?patchFlag,?dynamicProps,?shapeFlag,?isBlockNode,?true)
}
createVNode 的實現(xiàn)還是和之前類似,需要執(zhí)行一堆判斷邏輯,最終執(zhí)行 createBaseVNode 函數(shù)創(chuàng)建 vnode,注意這里 createBaseVNode 函數(shù)最后一個參數(shù)傳 true,也就是 needFullChildrenNormalization 為 true,那么在 createBaseVNode 的內(nèi)部,還需要多執(zhí)行 normalizeChildren 的邏輯。
組件 vnode 還是通過 createVNode 函數(shù)來創(chuàng)建。
總結(jié)
雖然看上去只是少執(zhí)行了幾行代碼,但由于大部分頁面都是由很多普通 DOM 元素構(gòu)成,創(chuàng)建普通元素 vnode 過程的優(yōu)化,對整體頁面的渲染和更新都會有很大的性能提升。
由于存在模板編譯的過程,Vue.js 可以利用編譯 + 運行時優(yōu)化,來實現(xiàn)整體的性能優(yōu)化。比如 Block Tree 的設(shè)計,就優(yōu)化了 diff 過程的性能。
其實對一個框架越了解,你就會越有敬畏之情,Vue.js 在編譯、運行時的實現(xiàn)都下了非常大的功夫,處理的細(xì)節(jié)很多,因此代碼的體積也難免變大。而且在框架已經(jīng)足夠成熟,有大量用戶使用的背景下還能從內(nèi)部做這么多的性能優(yōu)化,并且保證沒有 regression bug,實屬不易。
開源作品的用戶越多,受到的挑戰(zhàn)也會越大,需要考慮的細(xì)節(jié)就會越多,如果一個開源作品都沒啥人用,玩具級別,就真的別來碰瓷 Vue 了,根本不是一個段位的。
參考資料
[1]?Vue.js 3.2 升級介紹: https://blog.vuejs.org/posts/vue-3.2.html
[2] 相關(guān) PR: https://github.com/vuejs/vue-next/pull/3334
[3]?HcySunYang GitHub 地址:?https://github.com/HcySunYang
