你知道 Vue3 中的編譯最佳化嗎?

熊的貓發表於2023-03-03

前言

相比於 Vue2,Vue3 透過進行各種編譯最佳化,極大的提升了 Vue 的整體效能,這也使得 Vue3 在編譯和更新階段獲得了更快的速度提升!

接下來我們一起看看吧!

編譯最佳化

是什麼?

編譯最佳化 指的是編譯器將 模板(template) 編譯為 渲染函式(render) 的過程中,儘可能的 提取關鍵資訊,以達到 生成最優程式碼 的過程。

為什麼需要?

傳統的 Diff 演算法會存在很多無意義的對比操作

在對比 新舊 兩顆 虛擬 DOM 時,總是要按照 虛擬 DOM層級結構 "一層一層" 進行遍歷,然後其中某些內容的遍歷對比是完全沒必要的,例如:

<div id="foo">
    <p class="bar">{{ text }}</p>
</div>

其中唯一可能變化的就是 <p> 標籤中的 text 值,當響應式資料 text 發生修改時,最高效的更新方式就是直接更新 <p> 標籤對應的文字內容,然而對於 傳統 Diff 演算法 而言,會先根據 render 函式生成 新的虛擬 DOM,然後再對比 新舊虛擬 DOM 的方式:

  • 對比 div 節點,以及該節點的屬性和子節點
  • 對比 p 節點,以及該節點的屬性和子節點
  • 對比 p 節點的文字節點,發現文字節點發生變化,於是更新文字節點

    編譯思路

    Vue.js3 編譯最佳化的思路來源就是,跳過這些無意義的對操作,進一步的提升 VueDiff 演算法的對比效能:

  • 模板的結構相對穩定,在編譯階段儘可能提取關鍵資訊(如:標記靜態節點、動態節點)
  • 基於關鍵資訊,透過編譯器直接生成對應的原生 DOM 操作程式碼,減少生成 虛擬 DOM 的效能消耗,有利於提升初始化渲染的速度

    實驗性的新編譯策略

    image.png

從理論上來看,某些情況下確實並不需要 虛擬 DOM,(即 No Virtual DOM),但在 Vue.js3 中仍然選擇保留虛擬 DOM,並承受其帶來的效能開銷,主要是考慮到 渲染函式的靈活性Vue.js2 的相容性 問題.

感興趣可以去了解下,未來 Vue 會提供的一些新特性,不過這並不是本文的核心內容,State of Vue 2022-尤雨溪

Vue3 中的編譯最佳化的方式

標記動態節點

標記動態節點之後,在後續渲染器更新階段舊可以直接基於動態節點集合,實現對動態節點的 靶向更新定向更新.

patchFlag 屬性

在編譯器進行編譯時,如果判斷當前節點是屬於 動態節點,就會為這個 vnode 節點打上 patchFlag 標記,也就是新增一個 patchFlag 屬性,並且 patchFlag 屬性 對應的 數值 代表了當前這個 動態節點的型別,如:

  • 數字 1:代表該節點是 動態textContent
  • 數字 2:代表該節點是 動態calss 繫結
  • 數字 3:代表該節點是 動態style 繫結
  • ...

    dynamicChildren 屬性

    dynamicChildren 屬性 值對應的是一個陣列,其中儲存的就是帶有 patchFlag 屬性vnode 節點,並且帶有 dynamicChildren 屬性vnode 節點成稱為 塊,即 Block.

Block 節點

一個 Block 本質上也是一個 虛擬 DOM 節點,只不過它比普通的虛擬節點多了一個用於 儲存動態子節點dynamicChildren 屬性.

一個 Block 不僅能夠收集它的 直接動態子節點,也能收集所有 動態的子代節點,而後續渲染器的更新操作將以 Block 作為更新維度去處理.

什麼樣的節點會變成 Block 節點?
  • 所有模板的 根節點
  • 帶有 v-if 指令的節點
  • 帶有 v-for 指令的節點
  • 模板中 Frament 節點所包裹的 多根節點

其中 v-ifv-for 指令會導致 更新前後模板結構不穩定,不過由於 v-for 指令渲染的是一組子節點,為了更好的表示這一組子節點,就需要使用 Fragment 節點來表達 v-for 指令的渲染結果,並將其作為 Block 節點.

靜態提升

靜態提升的目的是儘可能減少更新時建立 虛擬 DOM 帶來的 效能開銷記憶體佔用.

沒有靜態提升時帶來的問題

通常,在響應式資料發生變化時,渲染函式就會重新執行,併產生新的虛擬 DOM 節點,顯然純靜態的虛擬節點完全沒有必要重新建立,這會帶來一定的效能開銷.

解決方案

在編譯階段可以 將純靜態節點提升到渲染函式外部,在渲染函式內部保持對靜態節點的引用即可,當響應式資料變化引起渲染函式重新執行時,並不會重新建立靜態的虛擬節點,這樣舊可以避免重複建立靜態節點的虛擬 DOM 帶來的效能開銷.

值得注意的是,靜態提升是以樹為單位的,畢竟不可能會為每一個小的靜態節點進行靜態提升,這會導致渲染函式外部對應儲存靜態節點的變數增多,這也會 佔用一定的記憶體.

預字串化

基於 靜態提升 可以繼續採用 預字串化 的最佳化手段,即直接將原本需要以樹為單位進行靜態提升的內容,直接轉換為對應基於 DOM 操作的 字串形式.

預字串化的優勢如下:

  • 大塊的靜態內容可以直接透過 innerHTML 進行設定,在 初始化渲染 時具有一定的效能優勢
  • 減少建立虛擬節點產生開銷的效能
  • 減少記憶體佔用

快取內聯事件處理函式

不快取內聯事件函式帶來的問題

在模板事件處理函式中,為了一些簡單的更新操作,通常會在模板中編寫 內聯的事件處理函式,例如:

<Comp @change="c = a + b">  ===>  function render(ctx){
                                     return h(Com, {
                                        // 內聯事件處理函式
                                        onChange: () => ctx.c = ctx.a + ctx.b
                                     })
                                  }

顯然,當 render 函式被重新執行時,都為會 Comp 元件建立一個全新的 props 物件,並且其中的 onChange 事件也是一個全新的函式,這會導致渲染器對 Comp 元件進行更新,造成額外的效能開銷。

解決方案

透過為 render 渲染函式傳遞第二個引數 cache 陣列,且這個 cache 陣列是來自於元件例項的,因此可以將內聯事件處理函式新增到 cache 陣列中快取起來.

當渲染函式重新執行時並建立虛擬 DOM 時,優先從快取中讀取對應的事件處理函式,避免事件處理函式被重新建立,導致 Comp 元件進行無用更新.

v-once 快取虛擬 DOM

Vue.js2Vue.js3 中都支援 v-once 指令,當前編譯器遇到 v-once 指令時,會利用上面提到的 cache 陣列來快取渲染函式的全部或部分執行結果.

v-once 的優勢

  • 避免元件更新時重新建立虛擬 DOM 帶來的效能開銷,因為虛擬 DOM 被快取了,因此更新時無需重新建立
  • 避免無用的 Diff 開銷,這是因為被 v-once 標記的虛擬 DOM 樹會被父級 Block 節點收集

最後

以上就是本文的全部內容,希望對你所有幫助!!!

本文參與了SegmentFault 思否寫作挑戰賽,歡迎正在閱讀的你也加入。

相關文章