再聊 Vue-js 3-2 關於 vnode 部分的優化
背景
上一篇文章,我分析了 Vue.js 3.2 關於響應式部分的優化,此外,在這次優化升級中,還有一個關於運行時的優化:
~200% faster creation of plain element VNodes
即針對普通元素類型 vnode
的創建,提升了約 200%
的性能。這也是一個非常偉大的優化,是 Vue 的官方核心開發者 HcySunYang 實現的,可以參考這個 PR。
那麼具體是怎麼做的呢,在分析實現前,我想先帶你瞭解一些 vnode
的背景知識。
什麼是 vnode
vnode
本質上是用來描述 DOM 的 JavaScript 對象,它在 Vue.js 中可以描述不同類型的節點,比如普通元素節點、組件節點等。
普通元素 vnode
什麼是普通元素節點呢?舉個例子,在 HTML 中我們使用 <button>
標籤來寫一個按鈕:
<button class="btn" style="width:100px;height:50px">click me</button>
我們可以用 vnode
這樣表示 <button>
標籤:
const vnode = {
type: 'button',
props: {
'class': 'btn',
style: {
width: '100px',
height: '50px'
}
},
children: 'click me'
}
其中,type
屬性表示 DOM 的標籤類型;props
屬性表示 DOM 的一些附加信息,比如 style
、class
等;children
屬性表示 DOM 的子節點,在該示例中它是一個簡單的文本字符串,當然,children
也可以是一個 vnode
數組。
組件 vnode
vnode
除了可以像上面那樣用於描述一個真實的 DOM,也可以用來描述組件。舉個例子,我們在模板中引入一個組件標籤 <custom-component>
:
<custom-component msg="test"></custom-component>
我們可以用 vnode
這樣表示 <custom-component>
組件標籤:
const CustomComponent = {
// 在這裏定義組件對象
}
const vnode = {
type: CustomComponent,
props: {
msg: 'test'
}
}
組件 vnode
其實是對抽象事物的描述,這是因爲我們並不會在頁面上真正渲染一個 <custom-component>
標籤,而最終會渲染組件內部定義的 HTML 標籤。
除了上述兩種 vnode
類型外,還有純文本 vnode
、註釋 vnode
等等。
另外,Vue.js 3.x 內部還針對 vnode
的 type
,做了更詳盡的分類,包括 Suspense
、Teleport
等,並且把 vnode
的類型信息做了編碼,以便在後面 vnode
的掛載階段,可以根據不同的類型執行相應的處理邏輯:
// 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 的優勢
知道什麼是 vnode
後,你可能會好奇,那麼 vnode
有什麼優勢呢?爲什麼一定要設計 vnode
這樣的數據結構呢?
首先是抽象,引入 vnode
,可以把渲染過程抽象化,從而使得組件的抽象能力也得到提升。
其次是跨平臺,因爲 patch vnode
的過程不同平臺可以有自己的實現,基於 vnode
再做服務端渲染、weex
平臺、小程序平臺的渲染都變得容易了很多。
不過這裏要特別注意,在瀏覽器端使用 vnode
並不意味着不用操作 DOM 了,很多人會誤以爲 vnode
的性能一定比手動操作原生 DOM 好,這個其實是不一定的。
因爲這種基於 vnode
實現的 MVVM 框架,在每次組件渲染生成 vnode
的過程中,會有一定的 JavaScript 耗時,尤其是是大組件。舉個例子,一個 1000 * 10
的 Table 組件,組件渲染生成 vnode
的過程會遍歷 1000 * 10
次去創建內部 cell vnode
,整個耗時就會變得比較長,再加上掛載 vnode
生成 DOM 的過程也會有一定的耗時,當我們去更新組件的時候,用戶會感覺到明顯的卡頓。
雖然 diff 算法在減少 DOM 操作方面足夠優秀,但最終還是免不了操作 DOM,所以說性能並不是 vnode
的優勢。
如何創建 vnode
通常我們開發組件都是編寫組件的模板,並不會手寫 vnode
,那麼 vnode
是如何創建的呢?
我們知道,組件模板經過編譯,會生成對應的 render
函數,在 render
函數內部,會執行 createVNode
函數創建 vnode
對象,我們來看一下 Vue.js 3.2 之前它的實現:
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
}
// 類組件的標準化
if (isClassComponent(type)) {
type = type.__vccOpts
}
// class 和 style 標準化.
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)
}
}
// 根據 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)
// 標準化 suspense 子節點
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
}
可以看到,創建 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
節點:
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
執行標準化,其中也會有一些判斷邏輯:
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)
}
}
接下來還會根據 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
然後就是創建 vnode
對象,創建完後還會執行 normalizeChildren
去標準化子節點,這個過程也會有一系列的判斷邏輯。
創建 vnode 過程的優化
仔細想想,vnode
本質上就是一個 JavaScript 對象,之所以在創建過程中做很多判斷,是因爲要處理各種各樣的情況。然而對於普通元素 vnode
而言,完全不需要這麼多的判斷邏輯,因此對於普通元素 vnode
,使用 createVNode
函數創建就是一種浪費。
順着這個思路,就可以在模板編譯階段,針對普通元素節點,使用新的函數來創建 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>
藉助於模板導出工具,可以看到它編譯後的 render
函數:
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
節點,這裏使用了 createElementVNode
方法而並非 createVNode
方法,而 createElementVNode
在內部是 createBaseVNode
的別名,來看它的實現:
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
內部僅僅是創建了 vnode
對象,然後做了一些 block
邏輯的處理。相比於之前的 createVNode
的實現,createBaseVNode
少執行了很多判斷邏輯,自然性能就獲得了提升。
而 createVNode
的實現,是基於 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
的實現還是和之前類似,需要執行一堆判斷邏輯,最終執行 createBaseVNode
函數創建 vnode
,注意這裏 createBaseVNode
函數最後一個參數傳 true
,也就是 needFullChildrenNormalization
爲 true
,那麼在 createBaseVNode
的內部,還需要多執行 normalizeChildren
的邏輯。
組件 vnode
還是通過 createVNode
函數來創建。
總結
雖然看上去只是少執行了幾行代碼,但由於大部分頁面都是由很多普通 DOM 元素構成,創建普通元素 vnode
過程的優化,對整體頁面的渲染和更新都會有很大的性能提升。
由於存在模板編譯的過程,Vue.js 可以利用編譯 + 運行時優化,來實現整體的性能優化。比如 Block Tree
的設計,就優化了 diff
過程的性能。
其實對一個框架越瞭解,你就會越有敬畏之情,Vue.js 在編譯、運行時的實現都下了非常大的功夫,處理的細節很多,因此代碼的體積也難免變大。而且在框架已經足夠成熟,有大量用戶使用的背景下還能從內部做這麼多的性能優化,並且保證沒有 regression bug,實屬不易。
開源作品的用戶越多,受到的挑戰也會越大,需要考慮的細節就會越多,如果一個開源作品都沒啥人用,玩具級別,就真的別來碰瓷 Vue 了,根本不是一個段位的。
參考資料
[1] Vue.js 3.2 升級介紹: https://blog.vuejs.org/posts/vue-3.2.html
[2] 相關 PR: https://github.com/vuejs/vue-next/pull/3334
[3] HcySunYang GitHub 地址: https://github.com/HcySunYang
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/z2ZCUFfFzp3c1ly4IQrrMA