⑴ 淺談vue3的編譯優化
編譯優化:編譯器將模版編譯為渲染函數的過程中,盡可能地提取關鍵信息,並以此指導生成最優代碼的過程。
優化的方向:盡可能地區分動態內容和靜態內容,並針對不同的內容採用不同的優化策略
1.動態節點收集與補丁標志1.1傳統diff演算法的問題比對新舊兩棵虛擬DOM樹的時候,總是要按照虛擬DOM的層級結構「一層一層」地遍歷
<divid="foo"><pclass="bar">{{text}}</p></div>上面這段代碼中,當響應式數據text值發生變化的時候,最高效的更新方式是直接設置p標簽的文本內容
傳統Diff演算法做不到如此高效,當text值發生變化的時候,會產生一顆新的虛擬DOM樹,對比新舊虛擬DOM過程如下:
對比div節點,以及該節點的屬性和子節點
對比p節點,以及該節點的屬性和子節點
對比p節點的文本子節點,如果文本子節點的內容變了,則更新,否則什麼都不做
可以發現,有很多無意義的對比操作。
總結:
傳統diff演算法的問題:無法利用編譯時提取到的任何關鍵信息,導致渲染器在運行時不會去做相關的優化。
vue3的編譯器會將編譯得到的關鍵信息「附著」在它生成的虛擬DOM上,傳遞給渲染器,執行「快捷路徑」。
1.2Block與PatchFlags傳統Diff演算法無法避免新舊虛擬DOM樹間無用的比較操作,是因為運行時得不到足夠的關鍵信息,從而無法區分動態內容和靜態內容。換句話說,只要運行時能夠區分動態內容和靜態內容,就可以實現極簡的優化策略
舉個例子:
<div><div>foo</div><p>{{bar}}</p></div>只有{{bar}}是動態的內容。理想情況下,當數據bar的值變化時,只需要更新p標簽的文本節點即可。為了實現這個目標,需要提供信息給運行時
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}可以發現,虛擬節點多了一個額外的屬性,即patchFlag(補丁標志),存在該屬性,就認為是動態節點
patchFlag(補丁標志)可以理解為一系列的數字標記,含義如下
constPatchFlags={TEXT:1,//代表節點有動態的textContentCLASS:2,//代表元素有動態的class綁定STYLE:3//其他。。。}可以在虛擬節點的創建階段,把它的動態子節點提取出來,並存儲到該虛擬節點的dynamicChildren數組中
constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點],//將children中的動態節點提取到dynamicChildren數組中dynamicChildren:[{tag:'p',children:ctx.bar,patchFlag:PatchFlags.TEXT}]}Block定義:帶有dynamicChildren屬性的虛擬節點稱為「塊」,即(Block)
一個Block本質上也是一個虛擬DOM,比普通的虛擬節點多處一個用來存儲動態節點的dynamicChildren屬性。(能夠收集所有的動態子代節點)
渲染器的更新操作會以Block為維度。當渲染器在更新一個Block時,會忽略虛擬節點的children數組,直接找到dynamicChildren數組,並只更新該數組中的動態節點。跳過了靜態內容,只更新動態內容。同時,由於存在對應的補丁標志,也能夠做到靶向更新。
Block節點有哪些:模版根節點、帶有v-for、v-if/v-else-if/v-else等指令的節點
1.3收集動態節點編譯器生成的渲染函數代碼中,不會直接包含用來描述虛擬節點的數據結構,而是包含著用來創建虛擬DOM節點的輔助函數,如下
render(){returncreateVNode('div',{id:'foo'},[createVNode('p',null,'text')])}functioncreateVNode(tag,props,children){constkey=props&&props.keyprops&&deleteprops.key//省略部分代碼return{tag,props,children,key}}createVNode的返回值是一個虛擬DOM節點
舉個例子:
<divid="foo"><pclass="bar">{{bar}}</p></div>上面模版生成帶有補丁標志的渲染函數如下:
render(){returncreateVNode('div',{id:'foo'},[createVNode('p',{class:'bar'},text,PatchFlags.TEXT)])}怎麼將根節點變成一個Block,如何將動態子代節點收集到該Block的dynamicChildren數組中?
可以發現,在渲染函數內,對createVNode函數的調用是層層嵌套結構,執行順序是內層先執行,外層再執行,當外層createVNode函數執行時,內層的createVNode函數已經執行完畢了。因此,為了讓外層Block節點能夠收集到內層動態節點,需要一個棧結構的數據來臨時存儲內層的動態節點。代碼實現如下:
//動態節點constdynamicChildrenStack=[]//當前動態節點集合letcurrentDynamicChildren=null//openBlock用來創建一個新的動態節點集合,並將該集合壓入棧中functionopenBlock(){dynamicChildrenStack.push((currentDynamicChildren=[]))}//closeBlock用來通過openBlock創建的動態節點集合從棧中彈出functioncloseBlock(){currentDynamicChildren=dynamicChildrenStack.pop()}然後調整createVNode函數
<div><div>foo</div><p>{{bar}}</p></div>0接著調整
<div><div>foo</div><p>{{bar}}</p></div>11.4.渲染器的運行時支持傳統的節點更新方式如下:
<div><div>foo</div><p>{{bar}}</p></div>2優化後的更新方式,直接對比動態節點
<div><div>foo</div><p>{{bar}}</p></div>3存在對應的補丁標志,可以針對性地完成靶向更新
<div><div>foo</div><p>{{bar}}</p></div>42.Block樹除了模版的根節點是Block外,帶有結構化指令的節點,如:v-if、v-for,也都應該是Block
2.1帶有v-if指令的節點<div><div>foo</div><p>{{bar}}</p></div>5假設只有最外層的div標簽會作為Block,那麼變數foo的值為true還是false,block收集到的動態節點都是一樣的,如下:
<div><div>foo</div><p>{{bar}}</p></div>6這意味著,在Diff階段不會更新。顯然,foo不同值下,一個是section,一個是div,是不同標簽,是需要更新的。
再舉個例子:
<div><div>foo</div><p>{{bar}}</p></div>7一樣會導致更新失敗
問題在於:dynamicChildren收集的動態節點是忽略虛擬DOM樹層級的,結構化指令會導致更新前後模版的結構發生變化,即模版結構不穩定
解決方法:讓帶有v-if/v-else-if/v-else等結構化指令的節點也作為Block即可,如下所示
<div><div>foo</div><p>{{bar}}</p></div>8<div><div>foo</div><p>{{bar}}</p></div>9在Diff過程中,渲染器根據key值區分,使用新的Block替換舊的Block
2.2帶有v-for指令的節點帶有v-for指令的節點也會讓虛擬DOM樹變得不穩定
例子:
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}0list的值由[1,2]變成[1]
更新前後對應的Block樹如下:
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}1更新前後,動態節點數量不一致,無法進行diff操作(diff操作的前提是:操作的節點必須是同層級節點,dynamicChildren不一定是同層級的)
解決方法:讓v-for指令的標簽也作為Block角色,保證虛擬DOM樹具有穩定的結構,無論v-for在運行時怎樣變化。如下:
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}2由於v-for指令渲染的是一個片段,所以類型用Fragment
2.3Fragment的穩定性//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}3發現Fragment本身收集的動態節點存在結構是不穩定的情況
結構不穩定:指更新前後一個block的dynamicChildren數組中收集的動態節點的數量或順序不一致
這種情況無法直接進行靶向更新
解決方法:回退到傳統虛擬DOM的Diff手段,即直接使用Fragment的children而非dynamicChildren來進行Diff操作
Fragment的子節點仍然可以是由Block組成的數組
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}4當Fragment的子節點更新時,就可以恢復優化模式
有穩定的Fragment嗎?如下:
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}5穩定的Fragment,可以使用優化模式
vue3模版中的多個根節點,也是穩定的Fragment
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}63.靜態提升減少更新時創建虛擬DOM帶來的性能開銷和內存佔用
如:
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}7沒有靜態提升時,渲染函數是:
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}8響應式數據title變化後,整個渲染函數會重新執行
把純靜態的節點提升到渲染函數之外
//傳統虛擬DOM描述constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar},]}9響應式數據title變化後,不會重新創建靜態的虛擬節點
註:靜態提升是以樹為單位的
包含動態綁定的節點本身不會被提升,但是該節點上的靜態屬性是可以被提升的
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}0可以減少創建虛擬DOM產生的開銷以及內存佔用
4.預字元串化基於靜態提升,進一步採用預字元串化優化。
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}1採用靜態提升優化策略後
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}2採用預字元串化將這些靜態節點序列化為字元串,並生成一個Static類型的VNode
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}3優勢:
大塊的靜態內容可以通過innerHTML設置,在性能上有一定優勢
減少創建虛擬節點產生的性能開銷
減少內存佔用
5.緩存內聯事件處理函數//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}4//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}5每次重新渲染時,都會為Com組件創建一個全新的props對象。同時,props對象中onChange屬性的值也會是全新的函數。造成額外的性能開銷
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}66.v-oncev-once可以對虛擬DOM進行緩存
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}7由於節點被緩存,意味著更新前後的虛擬節點不會發生變化,因此也就不需要這些被緩存的虛擬節點參與Diff操作了。編譯後的結果如下:
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}8v-once包裹的動態節點不會被父級Block收集,因此不會參與Diff操作
v-once指令通常用於不會發生改變的動態綁定中,例如綁定一個常量
//編譯優化後constvnode={tag:'div',children:[{tag:'div',children:'foo'},{tag:'p',children:ctx.bar,patchFlag:1},//這是動態節點]}9v-once帶來的性能提升
避免組件更新時重新創建虛擬DOM帶來的性能開銷。因為虛擬DOM被緩存了,所以更新時無需重新創建
避免無用的Diff開銷。因為被v-once標記的虛擬DOM樹不會被父級Block節點收集
7.總結1.vue3提出了Block的概念,利用Block樹及補丁標志
2.靜態提升:可以減少更新時創建虛擬DOM產生的性能開銷和內存佔用
3.預字元串化:在靜態提升的基礎上,對靜態節點進行字元串化。這樣做能夠減少創建虛擬節點產生的性能開銷以及內存佔用
4.緩存內聯事件處理函數:避免造成不必要的組件更新
5.v-once指令:緩存全部或部分虛擬節點,能夠避免組件更新時重新創建虛擬DOM帶來的性能開銷,也可以避免無用的Diff操作
原文:https://juejin.cn/post/7101859824203202568