大佬總結(jié),知乎轉(zhuǎn)載
https://zhuanlan.zhihu.com/p/150732926
image.png
Vue3
的 Compiler
與 runtime
緊密合作,充分利用編譯時(shí)信息,使得性能得到了極大的提升。本文的目的告訴你 Vue3
的 Compiler
到底做了哪些優(yōu)化,以及一些你可能希望知道的優(yōu)化細(xì)節(jié),在這個(gè)基礎(chǔ)上我們試著總結(jié)出一套手寫優(yōu)化模式的高性能渲染函數(shù)的方法,這些知識也可以用于實(shí)現(xiàn)一個(gè) Vue3
的 jsx babel
插件中,讓 jsx
也能享受優(yōu)化模式的運(yùn)行時(shí)收益,這里需要澄清的是,即使在非優(yōu)化模式下,理論上 Vue3
的 Diff
性能也是要優(yōu)于 Vue2
的。另外本文不包括 SSR
相關(guān)優(yōu)化,希望在下篇文章總結(jié)。
篇幅較大,花費(fèi)了很大的精力整理,對于對 Vue3
還沒有太多了解的同學(xué)閱讀起來也許會吃力,不妨先收藏,以后也許會用得到。
按照慣例 TOC:
-
Block Tree 和 PatchFlags
- 傳統(tǒng)
Diff
算法的問題 -
Block
配合PatchFlags
做到靶向更新 - 節(jié)點(diǎn)不穩(wěn)定 -
Block Tree
-
v-if
的元素作為Block
-
v-for
的元素作為Block
- 不穩(wěn)定的
Fragment
- 穩(wěn)定的
Fragment
-
v-for
的表達(dá)式是常量 - 多個(gè)根元素
- 插槽出口
<template v-for>
- 傳統(tǒng)
-
靜態(tài)提升
- 提升靜態(tài)節(jié)點(diǎn)樹
- 元素不會被提升的情況
- 元素帶有動態(tài)的
key
綁定 - 使用
ref
的元素 - 使用自定義指令的元素
- 提升靜態(tài)
PROPS
預(yù)字符串化
Cache Event handler
v-once
-
手寫高性能渲染函數(shù)
- 幾個(gè)需要記住的小點(diǎn)
-
Block Tree
是靈活的 - 正確地使用
PatchFlags
NEED_PATCH
- 該使用
Block
的地方必須用 - 分支判斷使用
Block
- 列表使用
Block
- 使用動態(tài)
key
的元素應(yīng)該是Block
- 使用
Slot hint
- 為組件正確地使用
DYNAMIC_SLOTS
- 使用
$stable hint
Block Tree 和 PatchFlags
Block Tree
和 PatchFlags
是 Vue3
充分利用編譯信息并在 Diff
階段所做的優(yōu)化。尤大已經(jīng)不止一次在公開場合聊過思路,我們深入細(xì)節(jié)的目的是為了更好的理解,并試圖手寫出高性能的 VNode
。
傳統(tǒng) Diff 算法的問題
“傳統(tǒng) vdom
”的 Diff
算法總歸要按照 vdom
樹的層級結(jié)構(gòu)一層一層的遍歷(如果你對各種傳統(tǒng) diff
算法不了解,可以看我之前寫《渲染器》這套文章,里面總結(jié)了三種傳統(tǒng) Diff
方式),舉個(gè)例子如下模板所示:
<div>
<p class="foo">bar</p>
</div>
對于傳統(tǒng) diff
算法來說,它在 diff
這段 vnode
(模板編譯后的 vnode
)時(shí)會經(jīng)歷:
Div 標(biāo)簽的屬性 + children
<p> 標(biāo)簽的屬性(class) + children
文本節(jié)點(diǎn):bar
但是很明顯,這明明就是一段靜態(tài) vdom
,它在組件更新階段是不可能發(fā)生變化的。如果能在 diff
階段跳過靜態(tài)內(nèi)容,那就會避免無用的 vdom
樹的遍歷和比對,這應(yīng)該就是最早的優(yōu)化思路來源----跳過靜態(tài)內(nèi)容,只對比動態(tài)內(nèi)容。
Block 配合 PatchFlags 做到靶向更新
咱們先說 Block
再聊 Block Tree
。現(xiàn)在思路有了,我們只希望對比非靜態(tài)的內(nèi)容,例如:
<div>
<p>foo</p>
<p>{{ bar }}</p>
</div>
在這段模板中,只有 <p>{{ bar }}</p>
中的文本節(jié)點(diǎn)是動態(tài)的,因此只需要靶向更新該文本節(jié)點(diǎn)即可,這在包含大量靜態(tài)內(nèi)容而只有少量動態(tài)內(nèi)容的場景下,性能優(yōu)勢尤其明顯。可問題是怎么做呢?我們需要拿到整顆 vdom
樹中動態(tài)節(jié)點(diǎn)的能力,其實(shí)可能沒有大家想像的復(fù)雜,來看下這段模板對應(yīng)的傳統(tǒng) vdom
樹大概長什么樣:
const vnode = {
tag: 'div',
children: [
{ tag: 'p', children: 'foo' },
{ tag: 'p', children: ctx.bar }, // 這是動態(tài)節(jié)點(diǎn)
]
}
在傳統(tǒng)的 vdom
樹中,我們在運(yùn)行時(shí)得不到任何有用信息,但是 Vue3
的 compiler
能夠分析模板并提取有用信息,最終體現(xiàn)在 vdom
樹上。例如它能夠清楚的知道:哪些節(jié)點(diǎn)是動態(tài)節(jié)點(diǎn),以及為什么它是動態(tài)的(是綁定了動態(tài)的 class
?還是綁定了動態(tài)的 style
?亦或是其它動態(tài)的屬性?),總之編譯器能夠提取我們想要的信息,有了這些信息我們就可以在創(chuàng)建 vnode
的過程中為動態(tài)的節(jié)點(diǎn)打上標(biāo)記:也就是傳說中的 PatchFlags
。
我們可以把 PatchFlags
簡單的理解為一個(gè)數(shù)字標(biāo)記,把這些數(shù)字賦予不同含義,例如:
- 數(shù)字 1:代表節(jié)點(diǎn)有動態(tài)的
textContent
(例如上面模板中的p
標(biāo)簽) - 數(shù)字 2:代表元素有動態(tài)的
class
綁定 - 數(shù)字 3:代表xxxxx
總之我們可以預(yù)設(shè)這些含義,最后體現(xiàn)在 vnode
上:
const vnode = {
tag: 'div',
children: [
{ tag: 'p', children: 'foo' },
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
]
}
有了這個(gè)信息,我們就可以在 vnode
的創(chuàng)建階段把動態(tài)節(jié)點(diǎn)提取出來,什么樣的節(jié)點(diǎn)是動態(tài)節(jié)點(diǎn)呢?帶有 patchFlag
的節(jié)點(diǎn)就是動態(tài)節(jié)點(diǎn),我們將它提取出來放到一個(gè)數(shù)組中存著,例如:
const vnode = {
tag: 'div',
children: [
{ tag: 'p', children: 'foo' },
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
],
dynamicChildren: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
]
}
dynamicChildren
就是我們用來存儲一個(gè)節(jié)點(diǎn)下所有子代動態(tài)節(jié)點(diǎn)的數(shù)組,注意這里的用詞哦:“子代”,例如:
const vnode = {
tag: 'div',
children: [
{ tag: 'section', children: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
]},
],
dynamicChildren: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
]
}
如上 vnode
所示,div
節(jié)點(diǎn)不僅能收集直接動態(tài)子節(jié)點(diǎn),它還能收集所有子代節(jié)點(diǎn)中的動態(tài)節(jié)點(diǎn)。為什么 div
節(jié)點(diǎn)這么厲害呢?因?yàn)樗鼡碛幸粋€(gè)特殊的角色:Block
,沒錯(cuò)這個(gè) div
節(jié)點(diǎn)就是傳說中的 Block
。一個(gè) Block
其實(shí)就是一個(gè) VNode
,只不過它有特殊的屬性(其中之一就是 dynamicChildren
)。
現(xiàn)在我們已經(jīng)拿到了所有的動態(tài)節(jié)點(diǎn),它們存儲在 dynamicChildren
中,因此在 diff
過程中就可以避免按照 vdom
樹一層一層的遍歷,而是直接找到 dynamicChildren
進(jìn)行更新。除了跳過無用的層級遍歷之外,由于我們早早的就為 vnode
打上了 patchFlag
,因此在更新 dynamicChildren
中的節(jié)點(diǎn)時(shí),可以準(zhǔn)確的知道需要為該節(jié)點(diǎn)應(yīng)用哪些更新動作,這基本上就實(shí)現(xiàn)了靶向更新。
節(jié)點(diǎn)不穩(wěn)定 - Block Tree
一個(gè) Block
怎么也構(gòu)不成 Block Tree
,這就意味著在一顆 vdom
樹中,會有多個(gè) vnode
節(jié)點(diǎn)充當(dāng) Block
的角色,進(jìn)而構(gòu)成一顆 Block Tree
。那么什么情況下一個(gè) vnode
節(jié)點(diǎn)會充當(dāng) block
的角色呢?
來看下面這段模板:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<div v-else>
<p>{{ a }}</p>
</div>
</div>
假設(shè)只要最外層的 div
標(biāo)簽是 Block
角色,那么當(dāng) foo
為真時(shí),block
收集到的動態(tài)節(jié)點(diǎn)為:
cosnt block = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.a, patchFlag: 1 }
]
}
當(dāng) foo
為假時(shí),block
的內(nèi)容如下:
cosnt block = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.a, patchFlag: 1 }
]
}
可以發(fā)現(xiàn)無論 foo
為真還是假,block
的內(nèi)容是不變的,這就意味什么在 diff
階段不會做任何更新,但是我們也看到了:v-if
的是一個(gè) <section>
標(biāo)簽,v-else
的是一個(gè) <div>
標(biāo)簽,所以這里就出問題了。實(shí)際上問題的本質(zhì)在于 dynamicChildren
的 diff
是忽略 vdom
樹層級的,如下模板也有同樣的問題:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<section v-else> <!-- 即使這里是 section -->
<div> <!-- 這個(gè) div 標(biāo)簽在 diff 過程中被忽略 -->
<p>{{ a }}</p>
</div>
</section >
</div>
即使 v-else
的也是一個(gè) <section>
標(biāo)簽,但由于前后 DOM
樹的不穩(wěn)定,也會導(dǎo)致問題。這時(shí)我們就思考,如何讓 DOM
樹的結(jié)構(gòu)變穩(wěn)定呢?
v-if
的元素作為 Block
如果讓使用了 v-if/v-else-if/v-else
等指令的元素也作為 Block
會怎么樣呢?我們拿如下模板為例:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<section v-else> <!-- 即使這里是 section -->
<div> <!-- 這個(gè) div 標(biāo)簽在 diff 過程中被忽略 -->
<p>{{ a }}</p>
</div>
</section >
</div>
如果我們讓這兩個(gè) section
標(biāo)簽都作為 block
,那么將構(gòu)成一顆 block tree
:
Block(Div)
- Block(Section v-if)
- Block(Section v-else)
父級 Block
除了會收集子代動態(tài)節(jié)點(diǎn)之外,也會收集子 Block
,因此兩個(gè) Block(section)
將作為 Block(div)
的 dynamicChildren
:
cosnt block = {
tag: 'div',
dynamicChildren: [
{ tag: 'section', { key: 0 }, dynamicChildren: [...]}, /* Block(Section v-if) */
{ tag: 'section', { key: 1 }, dynamicChildren: [...]} /* Block(Section v-else) */
]
}
這樣當(dāng) v-if
條件為真時(shí),dynamicChildren
中包含的是 Block(section v-if)
,當(dāng)條件為假時(shí) dynamicChildren
中包含的是 Block(section v-else)
,在 Diff 過程中,渲染器知道這是兩個(gè)不同的 Block
,因此會做完全的替換,這樣就解決了 DOM
結(jié)構(gòu)不穩(wěn)定引起的問題。而這就是 Block Tree
。
v-for 的元素作為 Block
不僅 v-if
會讓 DOM
結(jié)構(gòu)不穩(wěn)定,v-for
也會,但是 v-for
的情況稍微復(fù)雜一些。思考如下模板:
<div>
<p v-for="item in list">{{ item }}</p>
<i>{{ foo }}</i>
<i>{{ bar }}</i>
</div>
假設(shè) list 值由 [1 ,2]
變?yōu)?[1]
,按照之前的思路,最外層的 <div>
標(biāo)簽作為一個(gè) Block
,那么它更新前后對應(yīng)的 Block Tree
應(yīng)該是:
// 前
const prevBlock = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: 1, 1 /* TEXT */ },
{ tag: 'p', children: 2, 1 /* TEXT */ },
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
// 后
const nextBlock = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ },
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
prevBlcok
中有四個(gè)動態(tài)節(jié)點(diǎn),nextBlock
中有三個(gè)動態(tài)節(jié)點(diǎn)。這時(shí)候要如何進(jìn)行 Diff
?有的同學(xué)可能會說拿 dynamicChildren
進(jìn)行傳統(tǒng) Diff
,這是不對的,因?yàn)閭鹘y(tǒng) Diff
的一個(gè)前置條件是同層級節(jié)點(diǎn)間的 Diff
,但是 dynamicChildren
內(nèi)的節(jié)點(diǎn)未必是同層級的,這一點(diǎn)我們之前就提到過。
實(shí)際上我們只需要讓 v-for
的元素也作為一個(gè) Block
就可以了。這樣無論 v-for
怎么變化,它始終都是一個(gè) Block
,這保證了結(jié)構(gòu)穩(wěn)定,無論 v-for
怎么變化,這顆 Block Tree
看上去都是:
const block = {
tag: 'div',
dynamicChildren: [
// 這是一個(gè) Block 哦,它有 dynamicChildren
{ tag: Fragment, dynamicChildren: [/*.. v-for 的節(jié)點(diǎn) ..*/] }
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
不穩(wěn)定的 Fragment
剛剛我們使用一個(gè) Fragment
并讓它充當(dāng) Block
的角色解決了 v-for
元素所在層級的結(jié)構(gòu)穩(wěn)定,但我們來看一下這個(gè) Fragment
本身:
{ tag: Fragment, dynamicChildren: [/*.. v-for 的節(jié)點(diǎn) ..*/] }
對于如下這樣的模板:
<p v-for="item in list">{{ item }}</p>
在 list 由 [1, 2]
變成 [1]
的前后,Fragment
這個(gè) Block
看上去應(yīng)該是:
// 前
const prevBlock = {
tag: Fragment,
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ },
{ tag: 'p', children: item, 2 /* TEXT */ }
]
}
// 后
const prevBlock = {
tag: Fragment,
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ }
]
}
我們發(fā)現(xiàn),Fragment
這個(gè) Block
仍然面臨結(jié)構(gòu)不穩(wěn)定的情況,所謂結(jié)構(gòu)不穩(wěn)定從結(jié)果上看指的是更新前后一個(gè) block
的 dynamicChildren
中收集的動態(tài)節(jié)點(diǎn)數(shù)量或順序的不一致。這種不一致會導(dǎo)致我們沒有辦法直接進(jìn)行靶向 Diff
,怎么辦呢?其實(shí)對于這種情況是沒有辦法的,我們只能拋棄 dynamicChildren
的 Diff
,并回退到傳統(tǒng) Diff
:即 Diff
Fragment
的 children
而非 dynamicChildren
。
但需要注意的是 Fragment
的子節(jié)點(diǎn)(children
)仍然可以是 Block
:
const block = {
tag: Fragment,
children: [
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }
]
}
這樣,對于 <p>
標(biāo)簽及其子代節(jié)點(diǎn)的 Diff
將恢復(fù) Block Tree
的 Diff
模式。
穩(wěn)定的 Fragment
既然有不穩(wěn)定的 Fragment
,那就有穩(wěn)定的 Fragment
,什么樣的 Fragment
是穩(wěn)定的呢?
- v-for 的表達(dá)式是常量
<p v-for="n in 10"></p>
<!-- 或者 -->
<p v-for="s in 'abc'"></p>
由于 10
和 'abc'
是常量,所有這兩個(gè) Fragment
是不會變化的,因此它是穩(wěn)定的,對于穩(wěn)定的 Fragment
是不需要回退到傳統(tǒng) Diff
的,這在性能上會有一定的優(yōu)勢。
- 多個(gè)根元素
Vue3
不再限制組件的模板必須有一個(gè)根節(jié)點(diǎn),對于多個(gè)根節(jié)點(diǎn)的模板,例如:
<template>
<div></div>
<p></p>
<i></i>
</template>
如上,這也是一個(gè)穩(wěn)定的 Fragment,有的同學(xué)或許會想如下模板也是穩(wěn)定的 Fragment 嗎:
<template>
<div v-if="condition"></div>
<p></p>
<i></i>
</template>
這其實(shí)也是穩(wěn)定的,因?yàn)閹в?v-if
指令的元素本身作為 Block
存在,所以這段模板的 Block Tree
結(jié)構(gòu)總是:
Block(Fragment)
- Block(div v-if)
- VNode(p)
- VNode(i)
對應(yīng)到 VNode
應(yīng)該類似于:
const block = {
tag: Fragment,
dynamicChildren: [
{ tag: 'div', dynamicChildren: [...] },
{ tag: 'p' },
{ tag: 'i' },
],
PatchFlags.STABLE_FRAGMENT
}
無論如何,它的結(jié)構(gòu)都是穩(wěn)定的。需要注意的是這里的 PatchFlags.STABLE_FRAGMENT
,該標(biāo)志必須存在,否則會回退傳統(tǒng) Diff
模式。
- 插槽出口
如下模板所示:
<Comp>
<p v-if="ok"></p>
<i v-else></i>
</Comp>
組件 <Comp>
內(nèi)的 children
將作為插槽內(nèi)容,在經(jīng)過編譯后,應(yīng)該作為 Block
角色的內(nèi)容自然會是 Block
,已經(jīng)能夠保證結(jié)構(gòu)的穩(wěn)定了,例如如上代碼相當(dāng)于:
render(ctx) {
return createVNode(Comp, null, {
default: () => ([
ctx.ok
// 這里已經(jīng)是 Block 了
? (openBlock(), createBlock('p', { key: 0 }))
: (openBlock(), createBlock('i', { key: 1 }))
]),
_: 1 // 注意這里哦
})
}
既然結(jié)構(gòu)已經(jīng)穩(wěn)定了,那么在渲染出口處 Comp.vue
:
<template>
<slot/>
</template>
相當(dāng)于:
render() {
return (openBlock(), createBlock(Fragment, null,
this.$slots.default() || []
), PatchFlags.STABLE_FRAGMENT)
}
這自然就是 STABLE_FRAGMENT
,大家注意前面代碼中 _: 1
這是一個(gè)編譯的 slot hint
,當(dāng)我們手寫優(yōu)化模式的渲染函數(shù)時(shí)必須要使用這個(gè)標(biāo)志才能讓 runtime
知道 slot
是穩(wěn)定的,否則會退出非優(yōu)化模式。另外還有一個(gè) $stable
hint,在文末會講解。
- <template v-for>
如下模板所示:
<template>
<template v-for="item in list">
<p>{{ item.name }}</P>
<p>{{ item.age }}</P>
</template>
</template>
對于帶有 v-for
的 template
元素本身來說,它是一個(gè)不穩(wěn)定的 Fragment
,因?yàn)?list
不是常量。除此之外,由于 <template>
元素本身不渲染任何真實(shí) DOM
,因此如果它含有多個(gè)元素節(jié)點(diǎn),那么這些元素節(jié)點(diǎn)也將作為 Fragment
存在,但這個(gè) Fragment
是穩(wěn)定的,因?yàn)樗粫S著 list
的變化而變化。
以上內(nèi)容差不多就是 Block Tree
配合 PatchFlags
是如何做到靶向更新以及一些具體的思路細(xì)節(jié)了。
靜態(tài)提升
提升靜態(tài)節(jié)點(diǎn)樹
Vue3
的 Compiler
如果開啟了 hoistStatic
選項(xiàng)則會提升靜態(tài)節(jié)點(diǎn),或靜態(tài)的屬性,這可以減少創(chuàng)建 VNode
的消耗,如下模板所示:
<div>
<p>text</p>
</div>
在沒有被提升的情況下其渲染函數(shù)相當(dāng)于:
function render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', null, 'text')
]))
}
很明顯,p
標(biāo)簽是靜態(tài)的,它不會改變。但是如上渲染函數(shù)的問題也很明顯,如果組件內(nèi)存在動態(tài)的內(nèi)容,當(dāng)渲染函數(shù)重新執(zhí)行時(shí),即使 p
標(biāo)簽是靜態(tài)的,那么它對應(yīng)的 VNode
也會重新創(chuàng)建。當(dāng)開啟靜態(tài)提升后,其渲染函數(shù)如下:
const hoist1 = createVNode('p', null, 'text')
function render() {
return (openBlock(), createBlock('div', null, [
hoist1
]))
}
這就實(shí)現(xiàn)了減少 VNode
創(chuàng)建的性能消耗。需要了解的是,靜態(tài)提升是以樹為單位的,如下模板所示:
<div>
<section>
<p>
<span>abc</span>
</p>
</section >
</div>
除了根節(jié)點(diǎn)的 div
作為 block 不可被提升之外,整個(gè) <section>
元素及其子代節(jié)點(diǎn)都會被提升,因?yàn)樗麄兪钦脴涠际庆o態(tài)的。如果我們把上面代碼中的 abc
換成 {{ abc }}
,那么整棵樹都不會被提升。再看如下代碼:
<div>
<section>
{{ dynamicText }}
<p>
<span>abc</span>
</p>
</section >
</div>
由于 section
標(biāo)簽內(nèi)包含動態(tài)插值,因此以 section
為根節(jié)點(diǎn)的子樹就不會被提升,但是 p
標(biāo)簽以及其子代節(jié)點(diǎn)都是靜態(tài)的,是可以被提升的。
元素不會被提升的情況
- 元素帶有動態(tài)的
key
綁定
除了剛剛講到的元素的所有子代節(jié)點(diǎn)必須都是靜態(tài)的才會被提升之外還有哪些情況下會阻止提升呢?
如果一個(gè)元素有動態(tài)的 key
綁定那么它是不會被提升的,例如:
<div :key="foo"></div>
實(shí)際上一個(gè)元素?fù)碛腥魏蝿討B(tài)綁定都不應(yīng)該被提升,那么為什么 key
會被單獨(dú)拿出來?實(shí)際上 key
和普通的 props
相比,它對于 VNode
的意義是不一樣的,普通的 props
如果它是動態(tài)的,那么只需要體現(xiàn)在 PatchFlags
上就可以了,例如:
<div>
<p :foo="bar"></p>
</div>
我們可以為 p
標(biāo)簽打上 PatchFlags
:
render(ctx) {
return (openBlock(), createBlock('div', null, [
createVNode('p', { foo: ctx }, null, PatchFlags.PROPS, ['foo'])
]))
}
注意到在創(chuàng)建 VNode
時(shí),為其打上了 PatchFlags.PROPS
,代表這個(gè)元素需要更新 PROPS
,并且需要更新的 PROPS
的名字叫 foo
。
h但是 key
本身具有特殊意hi義,它是 VNode
(或元素) 的唯一標(biāo)識,即使兩個(gè)元素除了 key
以外一切都相同,但這兩個(gè)元素仍然是不同的元素,對于不同的元素需要做完全的替換處理才行,而 PatchFlags
用于在同一個(gè)元素上的屬性補(bǔ)丁,因此 key
是不同于其它 props
的。
正因?yàn)?key
的值是動態(tài)的可變的,因此對于擁有動態(tài) key
的元素,它始終都應(yīng)該參與到 diff
中并且不能簡單的打 PatchFlags
補(bǔ)丁標(biāo)識,那應(yīng)該怎么做呢?很簡單,讓擁有動態(tài) key
的元素也作為 Block
即可,以如下模板為例:
<div>
<div :key="foo"></div>
</div>
它對應(yīng)的渲染函數(shù)應(yīng)該是:
render(ctx) {
return (openBlock(), createBlock('div', null, [
(openBlock(), createBlock('div', { key: ctx.foo }))
]))
}
Tips:手寫優(yōu)化模式的渲染函數(shù)時(shí),如果使用動態(tài)的 key
,記得要使用 Block
哦,我們在后文還會總結(jié)。
- 使用
ref
的元素
如果一個(gè)元素使用了 ref
,無論是否動態(tài)綁定的值,那么這個(gè)元素都不會被靜態(tài)提升,這是因?yàn)樵诿恳淮?patch
時(shí)都需要設(shè)置 ref
的值,如下模板所示:
<div ref="domRef"></div>
乍一看覺得這完全就是一個(gè)靜態(tài)元素,沒錯(cuò),元素本身不會發(fā)生變化,但由于 ref
的特性,導(dǎo)致我們必須在每次 Diff
的過程中重新設(shè)置 ref
的值,為什么呢?來看一個(gè)使用 ref
的場景:
<template>
<div>
<p ref="domRef"></p>
</div>
</template>
<script>
export default {
setup() {
const refP1 = ref(null)
const refP2 = ref(null)
const useP1 = ref(true)
return {
domRef: useP1 ? refP1 : refP2
}
}
}
</script>
如上代碼所示,p
標(biāo)簽使用了一個(gè)非動態(tài)的 ref
屬性,值為字符串 domRef
,同時(shí)我們注意到 setupContext
(我們把 setup
函數(shù)返回的對象叫做 setupContext
) 中也包含了同名的 domRef
屬性,這不是偶然,他們之間會建立聯(lián)系,最終結(jié)果就是:
- 當(dāng)
useP1
為真時(shí),refP1.value
引用p
元素 - 當(dāng)
useP1
為假時(shí),refP2.value
引用p
元素
因此,即使 ref
是靜態(tài)的,但很顯然在更新的過程中由于 useP1
的變化,我們不得不更新 domRef
,所以只要一個(gè)元素使用了 ref
,它就不會被靜態(tài)提升,并且這個(gè)元素對應(yīng)的 VNode
也會被收集到父 Block
的 dynamicChildren
中。
但由于 p
標(biāo)簽除了需要更新 ref
之外,并不需要更新其他 props
,所以在真實(shí)的渲染函數(shù)中,會為它打上一個(gè)特殊的 PatchFlag
,叫做:PatchFlags.NEED_PATCH
:
render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', { ref: 'domRef' }, null, PatchFlags.NEED_PATCH)
]))
}
- 使用自定義指令的元素
實(shí)際上一個(gè)元素如果使用除 v-pre/v-cloak
之外的所有 Vue
原生提供的指令,都不會被提升,使用自定義指令也不會被提升,例如:
<p v-custom></p>
和使用 key
一樣,會為這段模板對應(yīng)的 VNode
打上 NEED_PATCH
標(biāo)志。順便講一下手寫渲染函數(shù)時(shí)如何應(yīng)用自定義指令,自定義指令是一種運(yùn)行時(shí)指令,與組件的生命周期類似,一個(gè) VNode
對象也有它自己生命周期:
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeUnmount
- unmounted
編寫一個(gè)自定義指令:
const myDir: Directive = {
beforeMount(el, binds) {
console.log(el)
console.log(binds.value)
console.log(binds.oldValue)
console.log(binds.arg)
console.log(binds.modifiers)
console.log(binds.instance)
}
}
使用該指令:
const App = {
setup() {
return () => {
return h('div', [
// 調(diào)用 withDirectives 函數(shù)
withDirectives(h('h1', 'hahah'), [
// 四個(gè)參數(shù)分別是:指令、值、參數(shù)、修飾符
[myDir, 10, 'arg', { foo: true }]
])
])
}
}
}
一個(gè)元素可以綁定多個(gè)指令:
const App = {
setup() {
return () => {
return h('div', [
// 調(diào)用 withDirectives 函數(shù)
withDirectives(h('h1', 'hahah'), [
// 四個(gè)參數(shù)分別是:指令、值、參數(shù)、修飾符
[myDir, 10, 'arg', { foo: true }],
[myDir2, 10, 'arg', { foo: true }],
[myDir3, 10, 'arg', { foo: true }]
])
])
}
}
}
提升靜態(tài) PROPS
前面說過,靜態(tài)節(jié)點(diǎn)的提升以樹為單位,如果一個(gè) VNode
存在非靜態(tài)的子代節(jié)點(diǎn),那么該 VNode
就不是靜態(tài)的,也就不會被提升。但這個(gè) VNode
的 props
卻可能是靜態(tài)的,這使我們可以將它的 props
進(jìn)行提升,這同樣可以節(jié)約 VNode
對象的創(chuàng)建開銷,內(nèi)存占用等,例如:
<div>
<p foo="bar" a=b>{{ text }}</p>
</div>
在這段模板中 p
標(biāo)簽有動態(tài)的文本內(nèi)容,因此不可以被提升,但 p
標(biāo)簽的所有屬性都是靜態(tài)的,因此可以提升它的屬性,經(jīng)過提升后其渲染函數(shù)如下:
const hoistProp = { foo: 'bar', a: 'b' }
render(ctx) {
return (openBlock(), createBlock('div', null, [
createVNode('p', hoistProp, ctx.text)
]))
}
即使動態(tài)綁定的屬性值,但如果值是常量,那么也會被提升:
<p :foo="10" :bar="'abc' + 'def'">{{ text }}</p>
'abc' + 'def'
是常量,可以被提升。
預(yù)字符串化
靜態(tài)提升的 VNode
節(jié)點(diǎn)或節(jié)點(diǎn)樹本身是靜態(tài)的,那么能否將其預(yù)先字符串化呢?如下模板所示:
<div>
<p></p>
<p></p>
...20 個(gè) p 標(biāo)簽
<p></p>
</div>
假設(shè)如上模板中有大量連續(xù)的靜態(tài)的 p
標(biāo)簽,當(dāng)采用了 hoist
優(yōu)化時(shí),結(jié)果如下:
cosnt hoist1 = createVNode('p', null, null, PatchFlags.HOISTED)
cosnt hoist2 = createVNode('p', null, null, PatchFlags.HOISTED)
... 20 個(gè) hoistx 變量
cosnt hoist20 = createVNode('p', null, null, PatchFlags.HOISTED)
render() {
return (openBlock(), createBlock('div', null, [
hoist1, hoist2, ...20 個(gè)變量, hoist20
]))
}
預(yù)字符串化會將這些靜態(tài)節(jié)點(diǎn)序列化為字符串并生成一個(gè) Static
類型的 VNode
:
const hoistStatic = createStaticVNode('<p></p><p></p><p></p>...20個(gè)...<p></p>')
render() {
return (openBlock(), createBlock('div', null, [
hoistStatic
]))
}
這有幾個(gè)明顯的優(yōu)勢:
- 生成代碼的體積減少
- 減少創(chuàng)建 VNode 的開銷
- 減少內(nèi)存占用
靜態(tài)節(jié)點(diǎn)在運(yùn)行時(shí)會通過 innerHTML
來創(chuàng)建真實(shí)節(jié)點(diǎn),因此并非所有靜態(tài)節(jié)點(diǎn)都是可以預(yù)字符串化的,可以預(yù)字符串化的靜態(tài)節(jié)點(diǎn)需要滿足以下條件:
非表格類標(biāo)簽:caption 、thead、tr、th、tbody、td、tfoot、colgroup、col
標(biāo)簽的屬性必須是:
標(biāo)準(zhǔn) HTML attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
或 data-/aria- 類屬性
當(dāng)一個(gè)節(jié)點(diǎn)滿足這些條件時(shí)代表這個(gè)節(jié)點(diǎn)是可以預(yù)字符串化的,但是如果只有一個(gè)節(jié)點(diǎn),那么并不會將其字符串化,可字符串化的節(jié)點(diǎn)必須連續(xù)且達(dá)到一定數(shù)量才行:
- 如果節(jié)點(diǎn)沒有屬性,那么必須有連續(xù) 20 個(gè)及以上的靜態(tài)節(jié)點(diǎn)存在才行,例如:
<div>
<p></p>
<p></p>
... 20 個(gè) p 標(biāo)簽
<p></p>
</div>
或者在這些連續(xù)的節(jié)點(diǎn)中有 5 個(gè)及以上的節(jié)點(diǎn)是有屬性綁定的節(jié)點(diǎn):
<div>
<p id="a"></p>
<p id="b"></p>
<p id="c"></p>
<p id="d"></p>
<p id="e"></p>
</div>
這段節(jié)點(diǎn)的數(shù)量雖然沒有達(dá)到 20 個(gè),但是滿足 5 個(gè)節(jié)點(diǎn)有屬性綁定。
這些節(jié)點(diǎn)不一定是兄弟關(guān)系,父子關(guān)系也是可以的,只要閾值滿足條件即可,例如:
<div>
<p id="a">
<p id="b">
<p id="c">
<p id="d">
<p id="e"></p>
</p>
</p>
</p>
</p>
</div>
預(yù)字符串化會在編譯時(shí)計(jì)算屬性的值,例如:
<div>
<p :id="'id-' + 1">
<p :id="'id-' + 2">
<p :id="'id-' + 3">
<p :id="'id-' + 4">
<p :id="'id-' + 5"></p>
</p>
</p>
</p>
</p>
</div>
在與字符串化之后:
const hoistStatic = createStaticVNode('<p id="id-1"></p><p id="id-2"></p>.....<p id="id-5"></p>')
可見 id
屬性值時(shí)計(jì)算后的。
Cache Event handler
如下組件的模板所示:
<Comp @change="a + b" />
這段模板如果手寫渲染函數(shù)的話相當(dāng)于:
render(ctx) {
return h(Comp, {
onChange: () => (ctx.a + ctx.b)
})
}
很顯然,每次 render
函數(shù)執(zhí)行的時(shí)候,Comp
組件的 props
都是新的對象,onChange
也會是全新的函數(shù)。這會導(dǎo)致觸發(fā) Comp
組件的更新。
當(dāng) Vue3 Compiler 開啟 prefixIdentifiers
以及 cacheHandlers
時(shí),這段模板會被編譯為:
render(ctx, cache) {
return h(Comp, {
onChange: cache[0] || (cache[0] = ($event) => (ctx.a + ctx.b))
})
}
這樣即使多次調(diào)用渲染函數(shù)也不會觸發(fā) Comp
組件的更新,因?yàn)?Vue
在 patch
階段比對 props
時(shí)就會發(fā)現(xiàn) onChange
的引用沒變。
如上代碼中 render
函數(shù)的 cache
對象是 Vue
內(nèi)部在調(diào)用渲染函數(shù)時(shí)注入的一個(gè)數(shù)組,像下面這種:
render.call(ctx, ctx, [])
實(shí)際上,我們即使不依賴編譯也能手寫出具備 cache
能力的代碼:
const Comp = {
setup() {
// 在 setup 中定義 handler
const handleChange = () => {/* ... */}
return () => {
return h(AnthorComp, {
onChange: handleChange // 引用不變
})
}
}
}
因此我們最好不要寫出如下這樣的代碼:
const Comp = {
setup() {
return () => {
return h(AnthorComp, {
onChang(){/*...*/} // 每次渲染函數(shù)執(zhí)行,都是全新的函數(shù)
})
}
}
}
v-once
這是 Vue2
就支持的功能,v-once
是一個(gè)“很指令”的指令,因?yàn)樗褪墙o編譯器看的,當(dāng)編譯器遇到 v-once
時(shí),會利用我們剛剛講過的 cache
來緩存全部或者一部分渲染函數(shù)的執(zhí)行結(jié)果,例如如下模板:
<div>
<div v-once>{{ foo }}</div>
</div>
會被編譯為:
render(ctx, cache) {
return (openBlock(), createBlock('div', null, [
cache[1] || (cache[1] = h("div", null, ctx.foo, 1 /* TEXT */))
]))
}
這樣就緩存了這段 vnode
。既然 vnode
已經(jīng)被緩存了,后續(xù)的更新就都會讀取緩存的內(nèi)容,而不會重新創(chuàng)建 vnode
對象了,同時(shí)在 Diff 的過程中也就不需要這段 vnode 參與了,因此你通常會看到編譯后的代碼更接近如下內(nèi)容:
render(ctx, cache) {
return (openBlock(), createBlock('div', null, [
cache[1] || (
setBlockTracking(-1), // 阻止這段 VNode 被 Block 收集
cache[1] = h("div", null, ctx.foo, 1 /* TEXT */),
setBlockTracking(1), // 恢復(fù)
cache[1] // 整個(gè)表達(dá)式的值
)
]))
}
稍微解釋一下這段代碼,我們已經(jīng)講解過何為 “Block Tree”,而 openBlock()
和 createBlock()
函數(shù)用來創(chuàng)建一個(gè) Block
。而 setBlockTracking(-1)
則用來暫停收集的動作,所以在 v-once
編譯生成的代碼中你會看到它,這樣使用 v-once
包裹的內(nèi)容就不會被收集到父 Block
中,也就不參與 Diff
了。
所以,v-once
帶來的性能提升來自兩方面:
- 1、VNode 的創(chuàng)建開銷
- 2、無用的 Diff 開銷
但其實(shí)我們不通過模板編譯,一樣可以通過緩存 VNode
來減少 VNode
的創(chuàng)建開銷:
const Comp = {
setup() {
// 緩存 content
const content = h('div', 'xxxx')
return () => {
return h('section', content)
}
}
}
但這樣避免不了無用的 Diff
開銷,因?yàn)槲覀儧]有使用 Block Tree
優(yōu)化模式。
這里有必要提及的一點(diǎn)是:在 Vue2.5.18+ 以及 Vue3 中 VNode 是可重用的,例如我們可以在不同的地方多次使用同一個(gè) VNode 節(jié)點(diǎn):
const Comp = {
setup() {
const content = h('div', 'xxxx')
return () => {
// 多次渲染 content
return h('section', [content, content, content])
}
}
}
手寫高性能渲染函數(shù)
接下來我們將進(jìn)入重頭戲環(huán)節(jié),我們嘗試手寫優(yōu)化模式的渲染函數(shù)。
幾個(gè)需要記住的小點(diǎn):
- 一個(gè)
Block
就是一個(gè)特殊的VNode
,可以理解為它只是比普通VNode
多了一個(gè)dynamicChildren
屬性 -
createBlock()
函數(shù)和createVNode()
函數(shù)的調(diào)用簽名幾乎相同,實(shí)際上createBlock()
函數(shù)內(nèi)部就是封裝了createVNode()
,這再次證明Block
就是VNode
。 - 在調(diào)用
createBlock()
創(chuàng)建Block
前要先調(diào)用openBlock()
函數(shù),通常這兩個(gè)函數(shù)配合逗號運(yùn)算符一同出現(xiàn):
render() {
return (openBlock(), createBlock('div'))
}
Block Tree 是靈活的:
在之前的介紹中根節(jié)點(diǎn)以 Block
的角色存在的,但是根節(jié)點(diǎn)并不必須是 Block
,我們可以在任意節(jié)點(diǎn)開啟 Block
:
setup() {
return () => {
return h('div', [
(openBlock(), createBlock('p', null, [/*...*/]))
])
}
}
這也是可以的,因?yàn)殇秩酒髟?Diff
的過程中如果 VNode
帶有 dynamicChildren
屬性,會自動進(jìn)入優(yōu)化模式。但是我們通常會讓根節(jié)點(diǎn)充當(dāng) Block
角色。
正確地使用 PatchFlags:
PatchFlags
用來標(biāo)記一個(gè)元素需要更新的內(nèi)容,例如當(dāng)元素有動態(tài)的 class
綁定時(shí),我們需要使用 PatchFlags.CLASS
標(biāo)記:
const App = {
setup() {
const refOk = ref(true)
return () => {
return (openBlock(), createBlock('div', null, [
createVNode('p', { class: { foo: refOk.value } }, 'hello', PatchFlags.CLASS) // 使用 CLASS 標(biāo)記
]))
}
}
}
如果使用了錯(cuò)誤的標(biāo)記則可能導(dǎo)致更新失敗,下面列出詳細(xì)的標(biāo)記使用方式:
-
PatchFlags.CLASS
- 當(dāng)有動態(tài)的class
綁定時(shí)使用 -
PatchFlags.STYLE
- 當(dāng)有動態(tài)的style
綁定時(shí)使用,例如:
createVNode('p', { style: { color: refColor.value } }, 'hello', PatchFlags.STYLE)
-
PatchFlags.TEXT
- 當(dāng)有動態(tài)的文本節(jié)點(diǎn)是使用,例如:
createVNode('p', null, refText.value, PatchFlags.TEXT)
-
PatchFlags.PROPS
- 當(dāng)有除了class
和style
之外的其他動態(tài)綁定屬性時(shí),例如:
createVNode('p', { foo: refVal.value }, 'hello', PatchFlags.PROPS, ['foo'])
這里需要注意的是,除了要使用 PatchFlags.PROPS
之外,還要提供第五個(gè)參數(shù),一個(gè)數(shù)組,包含了動態(tài)屬性的名字。
-
PatchFlags.FULL_PROPS
- 當(dāng)有動態(tài)name
的props
時(shí)使用,例如:
createVNode('p', { [refKey.value]: 'val' }, 'hello', PatchFlags.FULL_PROPS)
實(shí)際上使用 FULL_PROPS
等價(jià)于對 props
的 Diff
與傳統(tǒng) Diff
一樣。其實(shí),如果覺得心智負(fù)擔(dān)大,我們大可以全部使用 FULL_PROPS
,這么做的好處是:
- 避免誤用
PatchFlags
導(dǎo)致的bug
- 減少心智負(fù)擔(dān)的同時(shí),雖然失去了
props diff
的性能優(yōu)勢,但是仍然可以享受Block Tree
的優(yōu)勢。
當(dāng)同時(shí)存在多種更新,需要將 PatchFlags
進(jìn)行按位或運(yùn)算,例如:PatchFlags.CLASS | PatchFlags.STYLE
。
NEED_PATCH 標(biāo)識
為什么單獨(dú)把這個(gè)標(biāo)志拿出來講呢,它比較特殊,需要我們額外注意。當(dāng)我們使用 ref
或 onVNodeXXX
等 hook 時(shí)(包括自定義指令),需要使用該標(biāo)志,以至于它可以被父級 Block
收集,詳細(xì)原因我們在靜態(tài)提升一節(jié)里面講解過了:
const App = {
setup() {
const refDom = ref(null)
return () => {
return (openBlock(), createBlock('div', null,[
createVNode('p',
{
ref: refDom,
onVnodeBeforeMount() {/* ... */}
},
null,
PatchFlags.NEED_PATCH
)
]))
}
}
}
該使用 Block 的地方必須用
在最開始的時(shí)候,我們講解了有些指令會導(dǎo)致 DOM 結(jié)構(gòu)不穩(wěn)定,從而必須使用 Block 來解決問題。手寫渲染函數(shù)也是一樣:
- 分支判斷使用 Block:
const App = {
setup() {
const refOk = ref(true)
return () => {
return (openBlock(), createBlock('div', null, [
refOk.value
// 這里使用 Block
? (openBlock(), createBlock('div', { key: 0 }, [/* ... */]))
: (openBlock(), createBlock('div', { key: 1 }, [/* ... */]))
]))
}
}
}
這里使用 Block
的原因我們在前文已經(jīng)講解過了,但這里需要強(qiáng)調(diào)的是,除了分支判斷要使用 Block
之外,還需要為 Block
指定不同的 key
才行。
- 列表使用 Block:
當(dāng)我們渲染列表時(shí),我們常常寫出如下代碼:
const App = {
setup() {
const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })
return () => {
return (openBlock(), createBlock('div', null,
// 渲染列表
obj.list.map(item => {
return createVNode('p', null, item.val, PatchFlags.TEXT)
})
))
}
}
}
這么寫在非優(yōu)化模式下是沒問題的,但我們現(xiàn)在使用了 Block
,前文已經(jīng)講過為什么 v-for
需要使用 Block
的原因,試想當(dāng)我們執(zhí)行如下語句修改數(shù)據(jù):
obj.list.splice(0, 1)
這就會導(dǎo)致 Block
中收集的動態(tài)節(jié)點(diǎn)不一致,最終 Diff
出現(xiàn)問題。解決方案就是讓整個(gè)列表作為一個(gè) Block
,這時(shí)我們需要使用 Fragment
:
const App = {
setup() {
const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })
return () => {
return (openBlock(), createBlock('div', null, [
// 創(chuàng)建一個(gè) Fragment,并作為 Block 角色
(openBlock(true), createBlock(Fragment, null,
// 在這里渲染列表
obj.list.map(item => {
return createVNode('p', null, item.val, PatchFlags.TEXT)
}),
// 記得要指定正確的 PatchFlags
PatchFlags.UNKEYED_FRAGMENT
))
]))
}
}
}
總結(jié)一下:
- 對于列表我們應(yīng)該始終使用
Fragment
,并作為Block
的角色 - 如果
Fragment
的children
沒有指定key
,那么應(yīng)該為Fragment
打上PatchFlags.UNKEYED_FRAGMENT
。相應(yīng)的,如果指定了key
就應(yīng)該打上PatchFlags.KEYED_FRAGMENT
- 注意到在調(diào)用
openBlock(true)
時(shí),傳遞了參數(shù)true
,這代表這個(gè)Block
不會收集dynamicChildren
,因?yàn)闊o論是KEYED
還是UNKEYED
的Fragment
,在 Diff 它的children
時(shí)都會回退傳統(tǒng) Diff 模式,因此不需要收集dynamicChildren
。
這里還有一點(diǎn)需要注意,在 Diff Fragment 時(shí),由于回退了傳統(tǒng) Diff,我們希望盡快恢復(fù)優(yōu)化模式,同時(shí)保證后續(xù)收集的可控性,因此通常會讓 Fragment
的每一個(gè)子節(jié)點(diǎn)都作為 Block
的角色:
const App = {
setup() {
const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })
return () => {
return (openBlock(), createBlock('div', null, [
(openBlock(true), createBlock(Fragment, null,
obj.list.map(item => {
// 修改了這里
return (openBlock(), createBlock('p', null, item.val, PatchFlags.TEXT))
}),
PatchFlags.UNKEYED_FRAGMENT
))
]))
}
}
}
最后再說一下穩(wěn)定的 Fragment
,如果你能確定列表永遠(yuǎn)不會變化,例如你能確定 obj.list
是不會變化的,那么你應(yīng)該使用:PatchFlags.STABLE_FRAGMENT
標(biāo)志,并且調(diào)用 openBlcok()
去掉參數(shù),代表收集 dynamicChildren
:
const App = {
setup() {
const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })
return () => {
return (openBlock(), createBlock('div', null, [
// 調(diào)用 openBlock() 不要傳參
(openBlock(), createBlock(Fragment, null,
obj.list.map(item => {
// 列表中的任何節(jié)點(diǎn)都不需要是 Block 角色
return createVNode('p', null, item.val, PatchFlags.TEXT)
}),
// 穩(wěn)定的片段
PatchFlags.STABLE_FRAGMENT
))
]))
}
}
}
如上注釋所述。
- 使用動態(tài) key 的元素應(yīng)該是 Block
正如在靜態(tài)提升一節(jié)中所講的,當(dāng)元素使用動態(tài) key
的時(shí)候,即使兩個(gè)元素的其他方面完全一樣,那也是兩個(gè)不同的元素,需要做替換處理,在 Block Tree
中應(yīng)該以 Block
的角色存在,因此如果一個(gè)元素使用了動態(tài) key
,它應(yīng)該是一個(gè) Block
:
const App = {
setup() {
const refKey = ref('foo')
return () => {
return (openBlock(), createBlock('div', null,[
// 這里應(yīng)該是 Block
(openBlock(), createBlock('p', { key: refKey.value }))
]))
}
}
}
這實(shí)際上是必須的,詳情查看 https://github.com/vuejs/vue-next/issues/938 。
使用 Slot hint
我們在“穩(wěn)定的 Fragment”一節(jié)中提到了 slot hint
,當(dāng)我們?yōu)榻M件編寫插槽內(nèi)容時(shí),為了告訴 runtime:“我們已經(jīng)能夠保證插槽內(nèi)容的結(jié)構(gòu)穩(wěn)定”,則需要使用 slot hint
:
render() {
return (openBlock(), createBlock(Comp, null, {
default: () => [
refVal.value
? (openBlock(), createBlock('p', ...))
? (openBlock(), createBlock('div', ...))
],
// slot hint
_: 1
}))
}
當(dāng)然如果你不能保證這一點(diǎn),或者覺得心智負(fù)擔(dān)大,那么就不要寫 hint
了。
使用 $stable hint
$stable hint
和之前講的優(yōu)化策略不同,前文中的策略都是假設(shè)渲染器在優(yōu)化模式下工作的,而 $stable
用于非優(yōu)化模式,也就是我們平時(shí)寫的渲染函數(shù)。那么它有什么用呢?如下代碼所示(使用 tsx 演示):
export const App = defineComponent({
name: 'App',
setup() {
const refVal = ref(true)
return () => {
refVal.value
return (
<Hello>
{
{ default: () => [<p>hello</p>] }
}
</Hello>
)
}
}
})
如上代碼所示,渲染函數(shù)中讀取了 refVal.value
的值,建立了依賴收集關(guān)系,當(dāng)修改 refVal
的值時(shí),會觸發(fā) <Hello>
組件的更新,但是我們發(fā)現(xiàn) Hello
組件一來沒有 props
變化,二來它的插槽內(nèi)容是靜態(tài)的,因此不應(yīng)該更新才對,這時(shí)我們可以使用 $stable hint
:
export const App = defineComponent({
name: 'App',
setup() {
const refVal = ref(true)
return () => {
refVal.value
return (
<Hello>
{
{ default: () => [<p>hello</p>], $stable: true } // 修改了這里
}
</Hello>
)
}
}
})
為組件正確地使用 DYNAMIC_SLOTS
當(dāng)我們動態(tài)構(gòu)建 slots
時(shí),需要為組件的 VNode
指定 PatchFlags.DYNAMIC_SLOTS
,否則將導(dǎo)致更新失敗。什么是動態(tài)構(gòu)建 slots
呢?通常情況下是指:依賴當(dāng)前 scope
變量構(gòu)建的 slots
,例如:
render() {
// 使用當(dāng)前組件作用域的變量
const slots ={}
// 常見的場景
// 情況一:條件判斷
if (refVal.value) {
slots.header = () => [h('p', 'hello')]
}
// 情況二:循環(huán)
refList.value.forEach(item => {
slots[item.name] = () => [...]
})
// 情況三:動態(tài) slot 名稱,情況二包含情況三
slots[refName.value] = () => [...]
return (openBlock(), createBlock('div', null, [
// 這里要使用 PatchFlags.DYNAMIC_SLOTS
createVNode(Comp, null, slots, PatchFlags.DYNAMIC_SLOTS)
]))
}
如上注釋所述。
以上,不知道到達(dá)這里的同學(xué)有多少,Don't stop learning...