Vue.js3.2的vnode部分優(yōu)化升級使用示例詳解
背景
上一篇文章,分析了 Vue.js 3.2 關(guān)于響應(yīng)式部分的優(yōu)化,此外,在這次優(yōu)化升級中,還有一個運行時的優(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 中我們使用 <button>
標(biāo)簽來寫一個按鈕:
<button class="btn" style="width:100px;height:50px">click me</button>
我們可以用 vnode
這樣表示 <button>
標(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>
:
<custom-component msg="test"></custom-component>
我們可以用 vnode
這樣表示 <custom-component>
組件標(biāo)簽:
const CustomComponent = { // 在這里定義組件對象 } const vnode = { type: CustomComponent, props: { msg: 'test' } }
組件 vnode
其實是對抽象事物的描述,這是因為我們并不會在頁面上真正渲染一個 <custom-component>
標(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 了,根本不是一個段位的。
以上就是Vue.js3.2的vnode部分優(yōu)化升級使用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue.js vnode優(yōu)化升級的資料請關(guān)注腳本之家其它相關(guān)文章!
- vue3通過render函數(shù)實現(xiàn)菜單下拉框的示例
- vue中的render函數(shù)、h()函數(shù)、函數(shù)式組件詳解
- Vue render函數(shù)使用詳細(xì)講解
- Vue中render函數(shù)調(diào)用時機與執(zhí)行細(xì)節(jié)源碼分析
- 簡單談一談Vue中render函數(shù)
- vue中使用render封裝一個select組件
- Vue 2閱讀理解之initRender與callHook組件詳解
- vue語法之render函數(shù)和jsx的基本使用
- vue3中的render函數(shù)里定義插槽和使用插槽
- VUE3中h()函數(shù)和createVNode()函數(shù)的使用解讀
- Vue.js之VNode的使用
- vue中?render?函數(shù)功能與原理分析
相關(guān)文章
vue webpack開發(fā)訪問后臺接口全局配置的方法
今天小編就為大家分享一篇vue webpack開發(fā)訪問后臺接口全局配置的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09VUE安裝依賴時報錯:npm ERR! code ERESOLVE的解決
在使用npm安裝項目依賴時,有時會遇到錯誤信息 “npm ERR! code ERESOLVE”,本文就來介紹一下VUE安裝依賴時報錯的解決,具有一定的參考價值,感興趣的可以了解一下2023-10-10如何利用Vue3管理系統(tǒng)實現(xiàn)動態(tài)路由和動態(tài)側(cè)邊菜單欄
通常我們在vue項目中都是前端配置好路由的,但在一些項目中我們可能會遇到權(quán)限控制,這樣我們就涉及到動態(tài)路由的設(shè)置了,下面這篇文章主要給大家介紹了關(guān)于如何利用Vue3管理系統(tǒng)實現(xiàn)動態(tài)路由和動態(tài)側(cè)邊菜單欄的相關(guān)資料,需要的朋友可以參考下2022-02-02vue之組件內(nèi)監(jiān)控$store中定義變量的變化詳解
今天小編就為大家分享一篇vue之組件內(nèi)監(jiān)控$store中定義變量的變化詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11