Vue.js3.2的vnode部分優(yōu)化升級(jí)使用示例詳解
背景
上一篇文章,分析了 Vue.js 3.2 關(guān)于響應(yīng)式部分的優(yōu)化,此外,在這次優(yōu)化升級(jí)中,還有一個(gè)運(yùn)行時(shí)的優(yōu)化:
~200% faster creation of plain element VNodes
即針對(duì)普通元素類(lèi)型 vnode 的創(chuàng)建,提升了約 200% 的性能。這也是一個(gè)非常偉大的優(yōu)化,是 Vue 的官方核心開(kāi)發(fā)者 HcySunYang 實(shí)現(xiàn)的,可以參考這個(gè) PR。
那么具體是怎么做的呢,在分析實(shí)現(xiàn)前,我想先帶你了解一些 vnode 的背景知識(shí)。
什么是 vnode
vnode 本質(zhì)上是用來(lái)描述 DOM 的 JavaScript 對(duì)象,它在 Vue.js 中可以描述不同類(lèi)型的節(jié)點(diǎn),比如普通元素節(jié)點(diǎn)、組件節(jié)點(diǎn)等。
普通元素 vnode
什么是普通元素節(jié)點(diǎn)呢?舉個(gè)例子,在 HTML 中我們使用 <button> 標(biāo)簽來(lái)寫(xiě)一個(gè)按鈕:
<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)簽類(lèi)型;props 屬性表示 DOM 的一些附加信息,比如 style 、class 等;children 屬性表示 DOM 的子節(jié)點(diǎn),在該示例中它是一個(gè)簡(jiǎn)單的文本字符串,當(dāng)然,children 也可以是一個(gè) vnode 數(shù)組。
組件 vnode
vnode 除了可以像上面那樣用于描述一個(gè)真實(shí)的 DOM,也可以用來(lái)描述組件。舉個(gè)例子,我們?cè)谀0逯幸胍粋€(gè)組件標(biāo)簽 <custom-component>:
<custom-component msg="test"></custom-component>
我們可以用 vnode 這樣表示 <custom-component> 組件標(biāo)簽:
const CustomComponent = {
// 在這里定義組件對(duì)象
}
const vnode = {
type: CustomComponent,
props: {
msg: 'test'
}
}
組件 vnode 其實(shí)是對(duì)抽象事物的描述,這是因?yàn)槲覀儾⒉粫?huì)在頁(yè)面上真正渲染一個(gè) <custom-component> 標(biāo)簽,而最終會(huì)渲染組件內(nèi)部定義的 HTML 標(biāo)簽。
除了上述兩種 vnode 類(lèi)型外,還有純文本 vnode、注釋 vnode 等等。
另外,Vue.js 3.x 內(nèi)部還針對(duì) vnode 的 type,做了更詳盡的分類(lèi),包括 Suspense、Teleport 等,并且把 vnode 的類(lèi)型信息做了編碼,以便在后面 vnode 的掛載階段,可以根據(jù)不同的類(lèi)型執(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)勢(shì)
知道什么是 vnode 后,你可能會(huì)好奇,那么 vnode 有什么優(yōu)勢(shì)呢?為什么一定要設(shè)計(jì) vnode 這樣的數(shù)據(jù)結(jié)構(gòu)呢?
首先是抽象,引入 vnode,可以把渲染過(guò)程抽象化,從而使得組件的抽象能力也得到提升。
其次是跨平臺(tái),因?yàn)?patch vnode 的過(guò)程不同平臺(tái)可以有自己的實(shí)現(xiàn),基于 vnode 再做服務(wù)端渲染、weex 平臺(tái)、小程序平臺(tái)的渲染都變得容易了很多。
不過(guò)這里要特別注意,在瀏覽器端使用 vnode 并不意味著不用操作 DOM 了,很多人會(huì)誤以為 vnode 的性能一定比手動(dòng)操作原生 DOM 好,這個(gè)其實(shí)是不一定的。
因?yàn)檫@種基于 vnode 實(shí)現(xiàn)的 MVVM 框架,在每次組件渲染生成 vnode 的過(guò)程中,會(huì)有一定的 JavaScript 耗時(shí),尤其是是大組件。舉個(gè)例子,一個(gè) 1000 * 10 的 Table 組件,組件渲染生成 vnode 的過(guò)程會(huì)遍歷 1000 * 10 次去創(chuàng)建內(nèi)部 cell vnode,整個(gè)耗時(shí)就會(huì)變得比較長(zhǎng),再加上掛載 vnode 生成 DOM 的過(guò)程也會(huì)有一定的耗時(shí),當(dāng)我們?nèi)ジ陆M件的時(shí)候,用戶(hù)會(huì)感覺(jué)到明顯的卡頓。
雖然 diff 算法在減少 DOM 操作方面足夠優(yōu)秀,但最終還是免不了操作 DOM,所以說(shuō)性能并不是 vnode 的優(yōu)勢(shì)。
如何創(chuàng)建 vnode
通常我們開(kāi)發(fā)組件都是編寫(xiě)組件的模板,并不會(huì)手寫(xiě) vnode,那么 vnode 是如何創(chuàng)建的呢?
我們知道,組件模板經(jīng)過(guò)編譯,會(huì)生成對(duì)應(yīng)的 render 函數(shù),在 render 函數(shù)內(nèi)部,會(huì)執(zhí)行 createVNode 函數(shù)創(chuàng)建 vnode 對(duì)象,我們來(lái)看一下 Vue.js 3.2 之前它的實(shí)現(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
}
// 類(lèi)組件的標(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 的類(lèi)型編碼
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é)點(diǎn)
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 的過(guò)程做了很多事情,其中有很多判斷的邏輯,比如判斷 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 是不是一個(gè) vnode 節(jié)點(diǎn):
if (isVNode(type)) {
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
判斷 type 是不是一個(gè) class 類(lèi)型的組件:
if (isClassComponent(type)) {
type = type.__vccOpts
}
除此之外,還會(huì)對(duì)屬性中的 style 和 class 執(zhí)行標(biāo)準(zhǔn)化,其中也會(huì)有一些判斷邏輯:
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)
}
}
接下來(lái)還會(huì)根據(jù) vnode 的類(lèi)型編碼:
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 對(duì)象,創(chuàng)建完后還會(huì)執(zhí)行 normalizeChildren 去標(biāo)準(zhǔn)化子節(jié)點(diǎn),這個(gè)過(guò)程也會(huì)有一系列的判斷邏輯。
創(chuàng)建 vnode 過(guò)程的優(yōu)化
仔細(xì)想想,vnode 本質(zhì)上就是一個(gè) JavaScript 對(duì)象,之所以在創(chuàng)建過(guò)程中做很多判斷,是因?yàn)橐幚砀鞣N各樣的情況。然而對(duì)于普通元素 vnode 而言,完全不需要這么多的判斷邏輯,因此對(duì)于普通元素 vnode,使用 createVNode 函數(shù)創(chuàng)建就是一種浪費(fèi)。
順著這個(gè)思路,就可以在模板編譯階段,針對(duì)普通元素節(jié)點(diǎn),使用新的函數(shù)來(lái)創(chuàng)建 vnode,Vue.js 3.2 就是這么做的,舉個(gè)例子:
<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" })
])
]))
}
針對(duì)于 div 節(jié)點(diǎn),這里使用了 createElementVNode 方法而并非 createVNode 方法,而 createElementVNode 在內(nèi)部是 createBaseVNode 的別名,來(lái)看它的實(shí)現(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)部?jī)H僅是創(chuàng)建了 vnode 對(duì)象,然后做了一些 block 邏輯的處理。相比于之前的 createVNode 的實(shí)現(xiàn),createBaseVNode 少執(zhí)行了很多判斷邏輯,自然性能就獲得了提升。
而 createVNode 的實(shí)現(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 的實(shí)現(xiàn)還是和之前類(lèi)似,需要執(zhí)行一堆判斷邏輯,最終執(zhí)行 createBaseVNode 函數(shù)創(chuàng)建 vnode,注意這里 createBaseVNode 函數(shù)最后一個(gè)參數(shù)傳 true,也就是 needFullChildrenNormalization 為 true,那么在 createBaseVNode 的內(nèi)部,還需要多執(zhí)行 normalizeChildren 的邏輯。
組件 vnode 還是通過(guò) createVNode 函數(shù)來(lái)創(chuàng)建。
總結(jié)
雖然看上去只是少執(zhí)行了幾行代碼,但由于大部分頁(yè)面都是由很多普通 DOM 元素構(gòu)成,創(chuàng)建普通元素 vnode 過(guò)程的優(yōu)化,對(duì)整體頁(yè)面的渲染和更新都會(huì)有很大的性能提升。
由于存在模板編譯的過(guò)程,Vue.js 可以利用編譯 + 運(yùn)行時(shí)優(yōu)化,來(lái)實(shí)現(xiàn)整體的性能優(yōu)化。比如 Block Tree 的設(shè)計(jì),就優(yōu)化了 diff 過(guò)程的性能。
其實(shí)對(duì)一個(gè)框架越了解,你就會(huì)越有敬畏之情,Vue.js 在編譯、運(yùn)行時(shí)的實(shí)現(xiàn)都下了非常大的功夫,處理的細(xì)節(jié)很多,因此代碼的體積也難免變大。而且在框架已經(jīng)足夠成熟,有大量用戶(hù)使用的背景下還能從內(nèi)部做這么多的性能優(yōu)化,并且保證沒(méi)有 regression bug,實(shí)屬不易。
開(kāi)源作品的用戶(hù)越多,受到的挑戰(zhàn)也會(huì)越大,需要考慮的細(xì)節(jié)就會(huì)越多,如果一個(gè)開(kāi)源作品都沒(méi)啥人用,玩具級(jí)別,就真的別來(lái)碰瓷 Vue 了,根本不是一個(gè)段位的。
以上就是Vue.js3.2的vnode部分優(yōu)化升級(jí)使用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue.js vnode優(yōu)化升級(jí)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- vue3通過(guò)render函數(shù)實(shí)現(xiàn)菜單下拉框的示例
- vue中的render函數(shù)、h()函數(shù)、函數(shù)式組件詳解
- Vue render函數(shù)使用詳細(xì)講解
- Vue中render函數(shù)調(diào)用時(shí)機(jī)與執(zhí)行細(xì)節(jié)源碼分析
- 簡(jiǎn)單談一談Vue中render函數(shù)
- vue中使用render封裝一個(gè)select組件
- Vue 2閱讀理解之initRender與callHook組件詳解
- vue語(yǔ)法之render函數(shù)和jsx的基本使用
- vue3中的render函數(shù)里定義插槽和使用插槽
- VUE3中h()函數(shù)和createVNode()函數(shù)的使用解讀
- Vue.js之VNode的使用
- vue中?render?函數(shù)功能與原理分析
相關(guān)文章
vue webpack開(kāi)發(fā)訪(fǎng)問(wèn)后臺(tái)接口全局配置的方法
今天小編就為大家分享一篇vue webpack開(kāi)發(fā)訪(fǎng)問(wèn)后臺(tái)接口全局配置的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
Vue3中使用qrcode庫(kù)實(shí)現(xiàn)二維碼生成
Vue3中實(shí)現(xiàn)二維碼生成需要使用第三方庫(kù)來(lái)處理生成二維碼的邏輯,常用的庫(kù)有?qrcode和?vue-qrcode,本文主要介紹了Vue3中使用qrcode庫(kù)實(shí)現(xiàn)二維碼生成,感興趣的可以了解一下2023-12-12
一篇文章教會(huì)你部署vue項(xiàng)目到docker
在前端開(kāi)發(fā)中,部署項(xiàng)目是我們經(jīng)常發(fā)生的事情,下面這篇文章主要給大家介紹了關(guān)于部署vue項(xiàng)目到docker的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04
VUE安裝依賴(lài)時(shí)報(bào)錯(cuò):npm ERR! code ERESOLVE的解決
在使用npm安裝項(xiàng)目依賴(lài)時(shí),有時(shí)會(huì)遇到錯(cuò)誤信息 “npm ERR! code ERESOLVE”,本文就來(lái)介紹一下VUE安裝依賴(lài)時(shí)報(bào)錯(cuò)的解決,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
如何利用Vue3管理系統(tǒng)實(shí)現(xiàn)動(dòng)態(tài)路由和動(dòng)態(tài)側(cè)邊菜單欄
通常我們?cè)趘ue項(xiàng)目中都是前端配置好路由的,但在一些項(xiàng)目中我們可能會(huì)遇到權(quán)限控制,這樣我們就涉及到動(dòng)態(tài)路由的設(shè)置了,下面這篇文章主要給大家介紹了關(guān)于如何利用Vue3管理系統(tǒng)實(shí)現(xiàn)動(dòng)態(tài)路由和動(dòng)態(tài)側(cè)邊菜單欄的相關(guān)資料,需要的朋友可以參考下2022-02-02
vue之組件內(nèi)監(jiān)控$store中定義變量的變化詳解
今天小編就為大家分享一篇vue之組件內(nèi)監(jiān)控$store中定義變量的變化詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11

