Vue3 Compiler 優化細節,如何手寫高性能渲染函數
Vue3
的Compiler
與runtime
緊密合作,充分利用編譯時訊息,使得性能得到了極大的提升。本文的目的告訴你Vue3
的Compiler
到底做了哪些優化,以及一些你可能希望知道的優化細節,在這個基礎上我們試著總結出一套手寫優化模式的高性能渲染函數的方法,這些知識也可以用於實現一個Vue3
的jsx babel
插件中,讓jsx
也能享受優化模式的運行時收益,這裡需要澄清的是,即使在非優化模式下,理論上Vue3
的Diff
性能也是要優於Vue2
的。另外本文不包括SSR
相關優化,希望在下篇文章總結。
篇幅較大,花費了很大的精力整理,對於對Vue3
還沒有太多了解的同學閱讀起來也許會吃力,不妨先收藏,以後也許會用得到。
按照慣例TOC:
- Block Tree和PatchFlags
- 傳統
Diff
算法的問題 Block
配合PatchFlags
做到靶向更新- 節點不穩定-
Block Tree
-
v-if
的元素作為Block
-
v-for
的元素作為Block
- 不穩定的
Fragment
- 穩定的
Fragment
-
v-for
的表達式是常量 - 多個根元素
- 插槽出口
<template v-for>
- 靜態提升
- 提升靜態節點樹
- 元素不會被提升的情況
- 元素帶有動態的
key
綁定 - 使用
ref
的元素 - 使用自定義指令的元素
- 提升靜態
PROPS
- 預字符串化
- Cache Event handler
- v-once
- 手寫高性能渲染函數
- 幾個需要記住的小點
Block Tree
是靈活的- 正確地使用
PatchFlags
-
NEED_PATCH
- 該使用
Block
的地方必須用 - 分支判斷使用
Block
- 列表使用
Block
- 使用動態
key
的元素應該是Block
- 使用
Slot hint
- 為組件正確地使用
DYNAMIC_SLOTS
- 使用
$stable hint
Block Tree 和PatchFlags
Block Tree
和PatchFlags
是Vue3
充分利用編譯訊息並在Diff
階段所做的優化。尤大已經不止一次在公開場合聊過思路,我們深入細節的目的是為了更好的理解,並試圖手寫出高性能的VNode
。
傳統Diff算法的問題
“傳統vdom
”的Diff
算法總歸要按照vdom
樹的層級結構一層一層的遍歷(如果你對各種傳統diff
算法不了解,可以看我之前寫《渲染器》這套文章,裡面總結了三種傳統Diff
方式),舉個例子如下模板所示:
<div><pclass="foo">bar</p></div>
對於傳統diff
算法來說,它在diff
這段vnode
(模板編譯後的vnode
)時會經歷:
- Div 標籤的屬性+ children
- <p> 標籤的屬性(class) + children
- 文本節點:bar
但是很明顯,這明明就是一段靜態vdom
,它在組件更新階段是不可能發生變化的。如果能在diff
階段跳過靜態內容,那就會避免無用的vdom
樹的遍歷和比對,這應該就是最早的優化思路來源----跳過靜態內容,只對比動態內容。
Block配合PatchFlags做到靶向更新
咱們先說Block
再聊Block Tree
。現在思路有了,我們只希望對比非靜態的內容,例如:
<div><p>foo</p><p>{{ bar }}</p></div>
在這段模板中,只有<p>{{ bar }}</p>
中的文本節點是動態的,因此只需要靶向更新該文本節點即可,這在包含大量靜態內容而只有少量動態內容的場景下,性能優勢尤其明顯。可問題是怎麼做呢?我們需要拿到整顆vdom
樹中動態節點的能力,其實可能沒有大家想像的複雜,來看下這段模闆對應的傳統vdom
樹大概長什麼樣:
constvnode={tag:'div',children:[{tag:'p',children:'foo'},{tag:'p',children:ctx.bar},// 这是动态节点
]}
在傳統的vdom
樹中,我們在運行時得不到任何有用訊息,但是Vue3
的compiler
能夠分析模板並提取有用訊息,最終體現在vdom
樹上。例如它能夠清楚的知道:哪些節點是動態節點,以及為什麼它是動態的(是綁定了動態的class
?還是綁定了動態的style
?亦或是其它動態的屬性?),總之編譯器能夠提取我們想要的訊息,有了這些訊息我們就可以在創建vnode
的過程中為動態的節點打上標記:也就是傳說中的PatchFlags
。
我們可以把PatchFlags
簡單的理解為一個數字標記,把這些數字賦予不同含義,例如:
- 數字1:代表節點有動態的
textContent
(例如上面模板中的p
標籤) - 數字2:代表元素有動態的
class
綁定 - 數字3:代表xxxxx
總之我們可以預設這些含義,最後體現在vnode
上:
constvnode={tag:'div',children:[{tag:'p',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1/* 动态的 textContent */},]}
有了這個訊息,我們就可以在vnode
的創建階段把動態節點提取出來,什麼樣的節點是動態節點呢?帶有patchFlag
的節點就是動態節點,我們將它提取出來放到一個數組中存著,例如:
constvnode={tag:'div',children:[{tag:'p',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1/* 动态的 textContent */},],dynamicChildren:[{tag:'p',children:ctx.bar,patchFlag:1/* 动态的 textContent */},]}
dynamicChildren
就是我們用來存儲一個節點下所有子代動態節點的數組,注意這裡的用詞哦:“子代”,例如:
constvnode={tag:'div',children:[{tag:'section',children:[{tag:'p',children:ctx.bar,patchFlag:1/* 动态的 textContent */},]},],dynamicChildren:[{tag:'p',children:ctx.bar,patchFlag:1/* 动态的 textContent */},]}
如上vnode
所示, div
節點不僅能收集直接動態子節點,它還能收集所有子代節點中的動態節點。為什麼div
節點這麼厲害呢?因為它擁有一個特殊的角色: Block
,沒錯這個div
節點就是傳說中的Block
。一個Block
其實就是一個VNode
,只不過它有特殊的屬性(其中之一就是dynamicChildren
)。
現在我們已經拿到了所有的動態節點,它們存儲在dynamicChildren
中,因此在diff
過程中就可以避免按照vdom
樹一層一層的遍歷,而是直接找到dynamicChildren
進行更新。除了跳過無用的層級遍歷之外,由於我們早早的就為vnode
打上了patchFlag
,因此在更新dynamicChildren
中的節點時,可以準確的知道需要為該節點應用哪些更新動作,這基本上就實現了靶向更新。
節點不穩定- Block Tree
一個Block
怎麼也構不成Block Tree
,這就意味著在一顆vdom
樹中,會有多個vnode
節點充當Block
的角色,進而構成一顆Block Tree
。那麼什麼情況下一個vnode
節點會充當block
的角色呢?
來看下面這段模板:
<div><sectionv-if="foo"><p>{{ a }}</p></section><divv-else><p>{{ a }}</p></div></div>
假設只要最外層的div
標籤是Block
角色,那麼當foo
為真時, block
收集到的動態節點為:
cosntblock={tag:'div',dynamicChildren:[{tag:'p',children:ctx.a,patchFlag:1}]}
當foo
為假時, block
的內容如下:
cosntblock={tag:'div',dynamicChildren:[{tag:'p',children:ctx.a,patchFlag:1}]}
可以發現無論foo
為真還是假, block
的內容是不變的,這就意味什麼在diff
階段不會做任何更新,但是我們也看到了: v-if
的是一個<section>
標籤, v-else
的是一個<div>
標籤,所以這裡就出問題了。實際上問題的本質在於dynamicChildren
的diff
是忽略vdom
樹層級的,如下模板也有同樣的問題:
<div><sectionv-if="foo"><p>{{ a }}</p></section><sectionv-else><!-- 即使这里是 section --><div><!-- 这个 div 标签在 diff 过程中被忽略 --><p>{{ a }}</p></div></section></div>
即使v-else
的也是一個<section>
標籤,但由於前後DOM
樹的不穩定,也會導致問題。這時我們就思考,如何讓DOM
樹的結構變穩定呢?
v-if
的元素作為Block
如果讓使用了v-if/v-else-if/v-else
等指令的元素也作為Block
會怎麼樣呢?我們拿如下模板為例:
<div><sectionv-if="foo"><p>{{ a }}</p></section><sectionv-else><!-- 即使这里是 section --><div><!-- 这个 div 标签在 diff 过程中被忽略 --><p>{{ a }}</p></div></section></div>
如果我們讓這兩個section
標籤都作為block
,那麼將構成一顆block tree
:
Block(Div) - Block(Section v-if) - Block(Section v-else)
父級Block
除了會收集子代動態節點之外,也會收集子Block
,因此兩個Block(section)
將作為Block(div)
的dynamicChildren
:
cosntblock={tag:'div',dynamicChildren:[{tag:'section',{key:0},dynamicChildren:[...]},/* Block(Section v-if) */{tag:'section',{key:1},dynamicChildren:[...]}/* Block(Section v-else) */]}
這樣當v-if
條件為真時, dynamicChildren
中包含的是Block(section v-if)
,當條件為假時dynamicChildren
中包含的是Block(section v-else)
,在Diff過程中,渲染器知道這是兩個不同的Block
,因此會做完全的替換,這樣就解決了DOM
結構不穩定引起的問題。而這就是Block Tree
。
v-for的元素作為Block
不僅v-if
會讓DOM
結構不穩定, v-for
也會,但是v-for
的情況稍微複雜一些。思考如下模板:
<div><pv-for="item in list">{{ item }}</p><i>{{ foo }}</i><i>{{ bar }}</i></div>
假設list值由[1 ,2]
變為[1]
,按照之前的思路,最外層的<div>
標籤作為一個Block
,那麼它更新前後對應的Block Tree
應該是:
//前constprevBlock={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 */},]}//後constnextBlock={tag:'div',dynamicChildren:[{tag:'p',children:item,1/* TEXT */},{tag:'i',children:ctx.foo,1/* TEXT */},{tag:'i',children:ctx.bar,1/* TEXT */},]}
prevBlcok
中有四個動態節點, nextBlock
中有三個動態節點。這時候要如何進行Diff
?有的同學可能會說拿dynamicChildren
進行傳統Diff
,這是不對的,因為傳統Diff
的一個前置條件是同層級節點間的Diff
,但是dynamicChildren
內的節點未必是同層級的,這一點我們之前就提到過。
實際上我們只需要讓v-for
的元素也作為一個Block
就可以了。這樣無論v-for
怎麼變化,它始終都是一個Block
,這保證了結構穩定,無論v-for
怎麼變化,這顆Block Tree
看上去都是:
const block = { tag : 'div' , dynamicChildren : [ // 这是一个Block 哦,它有dynamicChildren { tag : Fragment , dynamicChildren : [ /*.. v-for 的节点..*/ ] } { tag : 'i' , children : ctx . foo , 1 /* TEXT */ }, { tag : 'i' , children : ctx . bar , 1 /* TEXT */ }, ] }
不穩定的Fragment
剛剛我們使用一個Fragment
並讓它充當Block
的角色解決了v-for
元素所在層級的結構穩定,但我們來看一下這個Fragment
本身:
{tag:Fragment,dynamicChildren:[/*.. v-for 的节点 ..*/]}
對於如下這樣的模板:
<pv-for="item in list">{{ item }}</p>
在list由[1, 2]
變成[1]
的前後, Fragment
這個Block
看上去應該是:
// 前
constprevBlock={tag:Fragment,dynamicChildren:[{tag:'p',children:item,1/* TEXT */},{tag:'p',children:item,2/* TEXT */}]}// 后
constprevBlock={tag:Fragment,dynamicChildren:[{tag:'p',children:item,1/* TEXT */}]}
我們發現, Fragment
這個Block
仍然面臨結構不穩定的情況,所謂結構不穩定從結果上看指的是更新前後一個block
的dynamicChildren
中收集的動態節點數量或順序的不一致。這種不一致會導致我們沒有辦法直接進行靶向Diff
,怎麼辦呢?其實對於這種情況是沒有辦法的,我們只能拋棄dynamicChildren
的Diff
,並回退到傳統Diff
:即Diff
Fragment
的children
而非dynamicChildren
。
但需要注意的是Fragment
的子節點( children
)仍然可以是Block
:
constblock={tag:Fragment,children:[{tag:'p',children:item,dynamicChildren:[/*...*/],1/* TEXT */},{tag:'p',children:item,dynamicChildren:[/*...*/],1/* TEXT */}]}
這樣,對於<p>
標籤及其子代節點的Diff
將恢復Block Tree
的Diff
模式。
穩定的Fragment
既然有不穩定的Fragment
,那就有穩定的Fragment
,什麼樣的Fragment
是穩定的呢?
- v-for的表達式是常量
<pv-for="n in 10"></p><!-- 或者 --><pv-for="s in 'abc'"></p>
由於10
和'abc'
是常量,所有這兩個Fragment
是不會變化的,因此它是穩定的,對於穩定的Fragment
是不需要回退到傳統Diff
的,這在性能上會有一定的優勢。
- 多個根元素
Vue3
不再限制組件的模板必須有一個根節點,對於多個根節點的模板,例如:
<template><div></div><p></p><i></i></template>
如上,這也是一個穩定的Fragment,有的同學或許會想如下模板也是穩定的Fragment 嗎:
<template><divv-if="condition"></div><p></p><i></i></template>
這其實也是穩定的,因為帶有v-if
指令的元素本身作為Block
存在,所以這段模板的Block Tree
結構總是:
Block(Fragment) - Block(div v-if) - VNode(p) - VNode(i)
對應到VNode
應該類似於:
constblock={tag:Fragment,dynamicChildren:[{tag:'div',dynamicChildren:[...]},{tag:'p'},{tag:'i'},],PatchFlags.STABLE_FRAGMENT}
無論如何,它的結構都是穩定的。需要注意的是這裡的PatchFlags.STABLE_FRAGMENT
,該標誌必須存在,否則會回退傳統Diff
模式。
- 插槽出口
如下模板所示:
<Comp><pv-if="ok"></p><iv-else></i></Comp>
組件<Comp>
內的children
將作為插槽內容,在經過編譯後,應該作為Block
角色的內容自然會是Block
,已經能夠保證結構的穩定了,例如如上代碼相當於:
render(ctx){returncreateVNode(Comp,null,{default:()=>([ctx.ok// 这里已经是 Block 了
?(openBlock(),createBlock('p',{key:0})):(openBlock(),createBlock('i',{key:1}))]),_:1// 注意这里哦
})}
既然結構已經穩定了,那麼在渲染出口處Comp.vue
:
<template><slot/></template>
相當於:
render(){return(openBlock(),createBlock(Fragment,null,this.$slots.default()||[]),PatchFlags.STABLE_FRAGMENT)}
這自然就是STABLE_FRAGMENT
,大家注意前面代碼中_: 1
這是一個編譯的slot hint
,當我們手寫優化模式的渲染函數時必須要使用這個標誌才能讓runtime
知道slot
是穩定的,否則會退出非優化模式。另外還有一個$stable
hint,在文末會講解。
- <template v-for>
如下模板所示:
<template><templatev-for="item in list"><p>{{ item.name }}</P><p>{{ item.age }}</P></template></template>
對於帶有v-for
的template
元素本身來說,它是一個不穩定的Fragment
,因為list
不是常量。除此之外,由於<template>
元素本身不渲染任何真實DOM
,因此如果它含有多個元素節點,那麼這些元素節點也將作為Fragment
存在,但這個Fragment
是穩定的,因為它不會隨著list
的變化而變化。
以上內容差不多就是Block Tree
配合PatchFlags
是如何做到靶向更新以及一些具體的思路細節了。
靜態提升
提升靜態節點樹
Vue3
的Compiler
如果開啟了hoistStatic
選項則會提升靜態節點,或靜態的屬性,這可以減少創建VNode
的消耗,如下模板所示:
<div><p>text</p></div>
在沒有被提升的情況下其渲染函數相當於:
functionrender(){return(openBlock(),createBlock('div',null,[createVNode('p',null,'text')]))}
很明顯, p
標籤是靜態的,它不會改變。但是如上渲染函數的問題也很明顯,如果組件內存在動態的內容,當渲染函數重新執行時,即使p
標籤是靜態的,那麼它對應的VNode
也會重新創建。當開啟靜態提升後,其渲染函數如下:
consthoist1=createVNode('p',null,'text')functionrender(){return(openBlock(),createBlock('div',null,[hoist1]))}
這就實現了減少VNode
創建的性能消耗。需要了解的是,靜態提升是以樹為單位的,如下模板所示:
<div><section><p><span>abc</span></p></section></div>
除了根節點的div
作為block不可被提升之外,整個<section>
元素及其子代節點都會被提升,因為他們是整棵樹都是靜態的。如果我們把上面代碼中的abc
換成{{ abc }}
,那麼整棵樹都不會被提升。再看如下代碼:
< div > < section > {{ dynamicText }} < p > < span > abc </ span > </ p > </ section > </ div >
由於section
標籤內包含動態插值,因此以section
為根節點的子樹就不會被提升,但是p
標籤以及其子代節點都是靜態的,是可以被提升的。
元素不會被提升的情況
- 元素帶有動態的
key
綁定
除了剛剛講到的元素的所有子代節點必須都是靜態的才會被提升之外還有哪些情況下會阻止提升呢?
如果一個元素有動態的key
綁定那麼它是不會被提升的,例如:
<div:key="foo"></div>
實際上一個元素擁有任何動態綁定都不應該被提升,那麼為什麼key
會被單獨拿出來?實際上key
和普通的props
相比,它對於VNode
的意義是不一樣的,普通的props
如果它是動態的,那麼只需要體現在PatchFlags
上就可以了,例如:
<div><p:foo="bar"></p></div>
我們可以為p
標籤打上PatchFlags
:
render(ctx){return(openBlock(),createBlock('div',null,[createVNode('p',{foo:ctx},null,PatchFlags.PROPS,['foo'])]))}
注意到在創建VNode
時,為其打上了PatchFlags.PROPS
,代表這個元素需要更新PROPS
,並且需要更新的PROPS
的名字叫foo
。
h但是key
本身俱有特殊意hi義,它是VNode
(或元素)的唯一標識,即使兩個元素除了key
以外一切都相同,但這兩個元素仍然是不同的元素,對於不同的元素需要做完全的替換處理才行,而PatchFlags
用於在同一個元素上的屬性補丁,因此key
是不同於其它props
的。
正因為key
的值是動態的可變的,因此對於擁有動態key
的元素,它始終都應該參與到diff
中並且不能簡單的打PatchFlags
補丁標識,那應該怎麼做呢?很簡單,讓擁有動態key
的元素也作為Block
即可,以如下模板為例:
<div><div:key="foo"></div></div>
它對應的渲染函數應該是:
render(ctx){return(openBlock(),createBlock('div',null,[(openBlock(),createBlock('div',{key:ctx.foo}))]))}
Tips:手寫優化模式的渲染函數時,如果使用動態的key
,記得要使用Block
哦,我們在後文還會總結。
- 使用
ref
的元素
如果一個元素使用了ref
,無論是否動態綁定的值,那麼這個元素都不會被靜態提升,這是因為在每一次patch
時都需要設置ref
的值,如下模板所示:
<divref="domRef"></div>
乍一看覺得這完全就是一個靜態元素,沒錯,元素本身不會發生變化,但由於ref
的特性,導致我們必須在每次Diff
的過程中重新設置ref
的值,為什麼呢?來看一個使用ref
的場景:
<template><div><pref="domRef"></p></div></template><script>exportdefault{setup(){constrefP1=ref(null)constrefP2=ref(null)constuseP1=ref(true)return{domRef:useP1?refP1:refP2}}}</script>
如上代碼所示, p
標籤使用了一個非動態的ref
屬性,值為字符串domRef
,同時我們注意到setupContext
(我們把setup
函數返回的對象叫做setupContext
)中也包含了同名的domRef
屬性,這不是偶然,他們之間會建立聯繫,最終結果就是:
- 當
useP1
為真時,refP1.value
引用p
元素 - 當
useP1
為假時,refP2.value
引用p
元素
因此,即使ref
是靜態的,但很顯然在更新的過程中由於useP1
的變化,我們不得不更新domRef
,所以只要一個元素使用了ref
,它就不會被靜態提升,並且這個元素對應的VNode
也會被收集到父Block
的dynamicChildren
中。
但由於p
標籤除了需要更新ref
之外,並不需要更新其他props
,所以在真實的渲染函數中,會為它打上一個特殊的PatchFlag
,叫做: PatchFlags.NEED_PATCH
:
render(){return(openBlock(),createBlock('div',null,[createVNode('p',{ref:'domRef'},null,PatchFlags.NEED_PATCH)]))}
- 使用自定義指令的元素
實際上一個元素如果使用除v-pre/v-cloak
之外的所有Vue
原生提供的指令,都不會被提升,使用自定義指令也不會被提升,例如:
<pv-custom></p>
和使用key
一樣,會為這段模闆對應的VNode
打上NEED_PATCH
標誌。順便講一下手寫渲染函數時如何應用自定義指令,自定義指令是一種運行時指令,與組件的生命週期類似,一個VNode
對像也有它自己生命週期:
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeUnmount
- unmounted
編寫一個自定義指令:
constmyDir: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)}}
使用該指令:
constApp={setup(){return()=>{returnh('div',[// 调用 withDirectives 函数
withDirectives(h('h1','hahah'),[// 四个参数分别是:指令、值、参数、修饰符
[myDir,10,'arg',{foo:true}]])])}}}
一個元素可以綁定多個指令:
constApp={setup(){return()=>{returnh('div',[//調用withDirectives函數withDirectives(h('h1','hahah'),[//四個參數分別是:指令、值、參數、修飾符[myDir,10,'arg',{foo:true}],[myDir2,10,'arg',{foo:true}],[myDir3,10,'arg',{foo:true}]])])}}}
提升靜態PROPS
前面說過,靜態節點的提升以樹為單位,如果一個VNode
存在非靜態的子代節點,那麼該VNode
就不是靜態的,也就不會被提升。但這個VNode
的props
卻可能是靜態的,這使我們可以將它的props
進行提升,這同樣可以節約VNode
對象的創建開銷,內存佔用等,例如:
<div><pfoo="bar"a=b>{{ text }}</p></div>
在這段模板中p
標籤有動態的文本內容,因此不可以被提升,但p
標籤的所有屬性都是靜態的,因此可以提升它的屬性,經過提升後其渲染函數如下:
consthoistProp={foo:'bar',a:'b'}render(ctx){return(openBlock(),createBlock('div',null,[createVNode('p',hoistProp,ctx.text)]))}
即使動態綁定的屬性值,但如果值是常量,那麼也會被提升:
<p:foo="10":bar="'abc' + 'def'">{{ text }}</p>
'abc' + 'def'
是常量,可以被提升。
預字符串化
靜態提升的VNode
節點或節點樹本身是靜態的,那麼能否將其預先字符串化呢?如下模板所示:
< div > < p ></ p > < p ></ p > ...20 个p 标签< p ></ p > </ div >
假設如上模板中有大量連續的靜態的p
標籤,當採用了hoist
優化時,結果如下:
cosnthoist1=createVNode('p',null,null,PatchFlags.HOISTED)cosnthoist2=createVNode('p',null,null,PatchFlags.HOISTED)...20個hoistx變量cosnthoist20=createVNode('p',null,null,PatchFlags.HOISTED)render(){return(openBlock(),createBlock('div',null,[hoist1,hoist2,...20個變量,hoist20]))}
預字符串化會將這些靜態節點序列化為字符串並生成一個Static
類型的VNode
:
consthoistStatic=createStaticVNode('<p></p><p></p><p></p>...20个...<p></p>')render(){return(openBlock(),createBlock('div',null,[hoistStatic]))}
這有幾個明顯的優勢:
- 生成代碼的體積減少
- 減少創建VNode 的開銷
- 減少內存佔用
靜態節點在運行時會通過innerHTML
來創建真實節點,因此並非所有靜態節點都是可以預字符串化的,可以預字符串化的靜態節點需要滿足以下條件:
- 非表格類標籤:caption 、thead、tr、th、tbody、td、tfoot、colgroup、col
- 標籤的屬性必須是:
- 標準HTML attribute: https:// developer.mozilla.org/e n-US/docs/Web/HTML/Attributes
- 或data-/aria- 類屬性
當一個節點滿足這些條件時代表這個節點是可以預字符串化的,但是如果只有一個節點,那麼並不會將其字符串化,可字符串化的節點必須連續且達到一定數量才行:
- 如果節點沒有屬性,那麼必須有連續20個及以上的靜態節點存在才行,例如:
< div > < p ></ p > < p ></ p > ... 20 个p 标签< p ></ p > </ div >
或者在這些連續的節點中有5個及以上的節點是有屬性綁定的節點:
<div><pid="a"></p><pid="b"></p><pid="c"></p><pid="d"></p><pid="e"></p></div>
這段節點的數量雖然沒有達到20 個,但是滿足5 個節點有屬性綁定。
這些節點不一定是兄弟關係,父子關係也是可以的,只要閾值滿足條件即可,例如:
<div><pid="a"><pid="b"><pid="c"><pid="d"><pid="e"></p></p></p></p></p></div>
預字符串化會在編譯時計算屬性的值,例如:
<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
屬性值時計算後的。
Cache Event handler
如下組件的模板所示:
<Comp@change="a + b"/>
這段模板如果手寫渲染函數的話相當於:
render(ctx){returnh(Comp,{onChange:()=>(ctx.a+ctx.b)})}
很顯然,每次render
函數執行的時候, Comp
組件的props
都是新的對象, onChange
也會是全新的函數。這會導致觸發Comp
組件的更新。
當Vue3 Compiler開啟prefixIdentifiers
以及cacheHandlers
時,這段模板會被編譯為:
render(ctx,cache){returnh(Comp,{onChange:cache[0]||(cache[0]=($event)=>(ctx.a+ctx.b))})}
這樣即使多次調用渲染函數也不會觸發Comp
組件的更新,因為Vue
在patch
階段比對props
時就會發現onChange
的引用沒變。
如上代碼中render
函數的cache
對像是Vue
內部在調用渲染函數時注入的一個數組,像下面這種:
render.call(ctx,ctx,[])
實際上,我們即使不依賴編譯也能手寫出具備cache
能力的代碼:
const Comp = { setup () { // 在setup 中定义handler const handleChange = () => { /* ... */ } return () => { return h ( AnthorComp , { onChange : handleChange // 引用不变}) } } }
因此我們最好不要寫出如下這樣的代碼:
constComp={setup(){return()=>{returnh(AnthorComp,{onChang(){/*...*/}// 每次渲染函数执行,都是全新的函数
})}}}
v-once
這是Vue2
就支持的功能, v-once
是一個“很指令”的指令,因為它就是給編譯器看的,當編譯器遇到v-once
時,會利用我們剛剛講過的cache
來緩存全部或者一部分渲染函數的執行結果,例如如下模板:
<div><divv-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
已經被緩存了,後續的更新就都會讀取緩存的內容,而不會重新創建vnode
對象了,同時在Diff的過程中也就不需要這段vnode參與了,因此你通常會看到編譯後的代碼更接近如下內容:
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),// 恢复
cache[1]// 整个表达式的值
)]))}
稍微解釋一下這段代碼,我們已經講解過何為“Block Tree”,而openBlock()
和createBlock()
函數用來創建一個Block
。而setBlockTracking(-1)
則用來暫停收集的動作,所以在v-once
編譯生成的代碼中你會看到它,這樣使用v-once
包裹的內容就不會被收集到父Block
中,也就不參與Diff
了。
所以, v-once
帶來的性能提升來自兩方面:
- 1、VNode 的創建開銷
- 2、無用的Diff 開銷
但其實我們不通過模板編譯,一樣可以通過緩存VNode
來減少VNode
的創建開銷:
const Comp = { setup () { // 缓存content const content = h ( 'div' , 'xxxx' ) return () => { return h ( 'section' , content ) } } }
但這樣避免不了無用的Diff
開銷,因為我們沒有使用Block Tree
優化模式。
這裡有必要提及的一點是:在Vue2.5.18+ 以及Vue3 中VNode 是可重用的,例如我們可以在不同的地方多次使用同一個VNode 節點:
const Comp = { setup () { const content = h ( 'div' , 'xxxx' ) return () => { // 多次渲染content return h ( 'section' , [ content , content , content ]) } } }
手寫高性能渲染函數
接下來我們將進入重頭戲環節,我們嘗試手寫優化模式的渲染函數。
幾個需要記住的小點:
- 一個
Block
就是一個特殊的VNode
,可以理解為它只是比普通VNode
多了一個dynamicChildren
屬性 createBlock()
函數和createVNode()
函數的調用簽名幾乎相同,實際上createBlock()
函數內部就是封裝了createVNode()
,這再次證明Block
就是VNode
。- 在調用
createBlock()
創建Block
前要先調用openBlock()
函數,通常這兩個函數配合逗號運算符一同出現:
render(){return(openBlock(),createBlock('div'))}
Block Tree是靈活的:
在之前的介紹中根節點以Block
的角色存在的,但是根節點並不必須是Block
,我們可以在任意節點開啟Block
:
setup(){return()=>{returnh('div',[(openBlock(),createBlock('p',null,[/*...*/]))])}}
這也是可以的,因為渲染器在Diff
的過程中如果VNode
帶有dynamicChildren
屬性,會自動進入優化模式。但是我們通常會讓根節點充當Block
角色。
正確地使用PatchFlags:
PatchFlags
用來標記一個元素需要更新的內容,例如當元素有動態的class
綁定時,我們需要使用PatchFlags.CLASS
標記:
constApp={setup(){constrefOk=ref(true)return()=>{return(openBlock(),createBlock('div',null,[createVNode('p',{class:{foo:refOk.value}},'hello',PatchFlags.CLASS)// 使用 CLASS 标记
]))}}}
如果使用了錯誤的標記則可能導致更新失敗,下面列出詳細的標記使用方式:
PatchFlags.CLASS
-當有動態的class
綁定時使用PatchFlags.STYLE
-當有動態的style
綁定時使用,例如:
createVNode('p',{style:{color:refColor.value}},'hello',PatchFlags.STYLE)
PatchFlags.TEXT
-當有動態的文本節點是使用,例如:
createVNode('p',null,refText.value,PatchFlags.TEXT)
PatchFlags.PROPS
-當有除了class
和style
之外的其他動態綁定屬性時,例如:
createVNode('p',{foo:refVal.value},'hello',PatchFlags.PROPS,['foo'])
這裡需要注意的是,除了要使用PatchFlags.PROPS
之外,還要提供第五個參數,一個數組,包含了動態屬性的名字。
PatchFlags.FULL_PROPS
-當有動態name
的props
時使用,例如:
createVNode('p',{[refKey.value]:'val'},'hello',PatchFlags.FULL_PROPS)
實際上使用FULL_PROPS
等價於對props
的Diff
與傳統Diff
一樣。其實,如果覺得心智負擔大,我們大可以全部使用FULL_PROPS
,這麼做的好處是:
- 避免誤用
PatchFlags
導致的bug
- 減少心智負擔的同時,雖然失去了
props diff
的性能優勢,但是仍然可以享受Block Tree
的優勢。
當同時存在多種更新,需要將PatchFlags
進行按位或運算,例如: PatchFlags.CLASS | PatchFlags.STYLE
。
NEED_PATCH標識
為什麼單獨把這個標誌拿出來講呢,它比較特殊,需要我們額外注意。當我們使用ref
或onVNodeXXX
等hook時(包括自定義指令),需要使用該標誌,以至於它可以被父級Block
收集,詳細原因我們在靜態提升一節裡面講解過了:
constApp={setup(){constrefDom=ref(null)return()=>{return(openBlock(),createBlock('div',null,[createVNode('p',{ref:refDom,onVnodeBeforeMount(){/* ... */}},null,PatchFlags.NEED_PATCH)]))}}}
該使用Block的地方必須用
在最開始的時候,我們講解了有些指令會導致DOM 結構不穩定,從而必須使用Block 來解決問題。手寫渲染函數也是一樣:
- 分支判斷使用Block:
constApp={setup(){constrefOk=ref(true)return()=>{return(openBlock(),createBlock('div',null,[refOk.value//這裡使用Block?(openBlock(),createBlock('div',{key:0},[/* ... */])):(openBlock(),createBlock('div',{key:1},[/* ... */]))]))}}}
這裡使用Block
的原因我們在前文已經講解過了,但這裡需要強調的是,除了分支判斷要使用Block
之外,還需要為Block
指定不同的key
才行。
- 列表使用Block:
當我們渲染列表時,我們常常寫出如下代碼:
constApp={setup(){constobj=reactive({list:[{val:1},{val:2}]})return()=>{return(openBlock(),createBlock('div',null,//渲染列表obj.list.map(item=>{returncreateVNode('p',null,item.val,PatchFlags.TEXT)})))}}}
這麼寫在非優化模式下是沒問題的,但我們現在使用了Block
,前文已經講過為什麼v-for
需要使用Block
的原因,試想當我們執行如下語句修改數據:
obj.list.splice(0,1)
這就會導致Block
中收集的動態節點不一致,最終Diff
出現問題。解決方案就是讓整個列表作為一個Block
,這時我們需要使用Fragment
:
constApp={setup(){constobj=reactive({list:[{val:1},{val:2}]})return()=>{return(openBlock(),createBlock('div',null,[//創建一個Fragment,並作為Block角色(openBlock(true),createBlock(Fragment,null,//在這裡渲染列表obj.list.map(item=>{returncreateVNode('p',null,item.val,PatchFlags.TEXT)}),//記得要指定正確的PatchFlagsPatchFlags.UNKEYED_FRAGMENT))]))}}}
總結一下:
- 對於列表我們應該始終使用
Fragment
,並作為Block
的角色 - 如果
Fragment
的children
沒有指定key
,那麼應該為Fragment
打上PatchFlags.UNKEYED_FRAGMENT
。相應的,如果指定了key
就應該打上PatchFlags.KEYED_FRAGMENT
- 注意到在調用
openBlock(true)
時,傳遞了參數true
,這代表這個Block
不會收集dynamicChildren
,因為無論是KEYED
還是UNKEYED
的Fragment
,在Diff它的children
時都會回退傳統Diff模式,因此不需要收集dynamicChildren
。
這裡還有一點需要注意,在Diff Fragment時,由於回退了傳統Diff,我們希望盡快恢復優化模式,同時保證後續收集的可控性,因此通常會讓Fragment
的每一個子節點都作為Block
的角色:
constApp={setup(){constobj=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))]))}}}
最後再說一下穩定的Fragment
,如果你能確定列表永遠不會變化,例如你能確定obj.list
是不會變化的,那麼你應該使用: PatchFlags.STABLE_FRAGMENT
標誌,並且調用openBlcok()
去掉參數,代表收集dynamicChildren
:
constApp={setup(){constobj=reactive({list:[{val:1},{val:2}]})return()=>{return(openBlock(),createBlock('div',null,[//調用openBlock()不要傳參(openBlock(),createBlock(Fragment,null,obj.list.map(item=>{//列表中的任何節點都不需要是Block角色returncreateVNode('p',null,item.val,PatchFlags.TEXT)}),//穩定的片段PatchFlags.STABLE_FRAGMENT))]))}}}
如上註釋所述。
- 使用動態key的元素應該是Block
正如在靜態提升一節中所講的,當元素使用動態key
的時候,即使兩個元素的其他方面完全一樣,那也是兩個不同的元素,需要做替換處理,在Block Tree
中應該以Block
的角色存在,因此如果一個元素使用了動態key
,它應該是一個Block
:
const App = { setup () { const refKey = ref ( 'foo' ) return () => { return ( openBlock (), createBlock ( 'div' , null ,[ // 这里应该是Block ( openBlock (), createBlock ( 'p' , { key : refKey . value })) ])) } } }
這實際上是必須的,詳情查看https:// github.com/vuejs/vue-ne xt/issues/938 。
使用Slot hint
我們在“穩定的Fragment”一節中提到了slot hint
,當我們為組件編寫插槽內容時,為了告訴runtime:“我們已經能夠保證插槽內容的結構穩定”,則需要使用slot hint
:
render () { return ( openBlock (), createBlock ( Comp , null , { default : () => [ refVal . value ? ( openBlock (), createBlock ( 'p' , ...)) ? ( openBlock (), createBlock ( 'div' , ...)) ], // slot hint _ : 1 })) }
當然如果你不能保證這一點,或者覺得心智負擔大,那麼就不要寫hint
了。
使用$stable hint
$stable hint
和之前講的優化策略不同,前文中的策略都是假設渲染器在優化模式下工作的,而$stable
用於非優化模式,也就是我們平時寫的渲染函數。那麼它有什麼用呢?如下代碼所示(使用tsx 演示):
exportconstApp=defineComponent({name:'App',setup(){constrefVal=ref(true)return()=>{refVal.valuereturn(<Hello>{{default:()=>[<p>hello</p>] }}</Hello>)}}})
如上代碼所示,渲染函數中讀取了refVal.value
的值,建立了依賴收集關係,當修改refVal
的值時,會觸發<Hello>
組件的更新,但是我們發現Hello
組件一來沒有props
變化,二來它的插槽內容是靜態的,因此不應該更新才對,這時我們可以使用$stable hint
:
exportconstApp=defineComponent({name:'App',setup(){constrefVal=ref(true)return()=>{refVal.valuereturn(<Hello>{{default:()=>[<p>hello</p>], $stable: true } //修改了这里}</Hello>)}}})
為組件正確地使用DYNAMIC_SLOTS
當我們動態構建slots
時,需要為組件的VNode
指定PatchFlags.DYNAMIC_SLOTS
,否則將導致更新失敗。什麼是動態構建slots
呢?通常情況下是指:依賴當前scope
變量構建的slots
,例如:
render(){//使用當前組件作用域的變量constslots={}//常見的場景//情況一:條件判斷if(refVal.value){slots.header=()=>[h('p','hello')]}//情況二:循環refList.value.forEach(item=>{slots[item.name]=()=>[...]})//情況三:動態slot名稱,情況二包含情況三slots[refName.value]=()=>[...]return(openBlock(),createBlock('div',null,[//這裡要使用PatchFlags.DYNAMIC_SLOTScreateVNode(Comp,null,slots,PatchFlags.DYNAMIC_SLOTS)]))}
如上註釋所述。
以上,不知道到達這裡的同學有多少,Don't stop learning...