2024-11-07|閱讀時間 ‧ 約 0 分鐘

EP61 - 渲染機制

Rendering Mechanism 一直不懂這是什麼!
希望多了解一點~應該對框架會更熟悉吧www

Vue 如何將模板轉換為實際的 DOM 節點?
Vue 如何高效地更新這些 DOM 節點?
我們將深入探討 Vue 的內部渲染機制,試圖解答這些問題。

虛擬 DOM - Virtual DOM

您可能聽說過 "虛擬 DOM" 這個術語,Vue 的渲染系統就是基於此概念的。

虛擬 DOM (VDOM) 是一種編程概念,其中將 UI 的理想或“虛擬”表示保存在內存中,並與“真實” DOM 同步。這一概念由 React 首創,並在許多其他框架中採用了不同的實現,包括 Vue。

虛擬 DOM 更像是一種模式而不是特定技術,所以沒有一個標準的實現。我們可以用一個簡單的例子來說明這個概念:

const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* more vnodes */
]
}

在這裡,vnode 是一個普通的 JavaScript 物件(“虛擬節點”),代表一個 <div> 元素。它包含創建實際元素所需的所有信息,也包含更多的子 vnode,使其成為虛擬 DOM 樹的根節點。

運行時渲染器可以遍歷虛擬 DOM 樹並從中構建真實的 DOM 樹。這個過程稱為掛載(mount)。

如果我們有兩個虛擬 DOM 樹的副本,渲染器還可以遍歷並比較這兩棵樹,找出差異,並將這些更改應用到實際 DOM 中。這個過程稱為修補(patch),也稱為“差異算法(diffing)”或“協調過程(reconcilication)”。

虛擬 DOM 的主要優勢在於,它使開發者能夠以聲明的方式程式化地創建、檢查和組合所需的 UI 結構,而將直接 DOM 操作留給渲染器處理。

渲染管線 - Render Pipeline

在高層次上,這是 Vue 元件掛載時發生的事情:

  1. 編譯(compile):Vue 模板被編譯成渲染函數,即返回虛擬 DOM 樹的函數。這個步驟可以在構建過程中提前完成,也可以通過運行時編譯器即時完成。
  2. 掛載(mount):運行時渲染器調用渲染函數,遍歷返回的虛擬 DOM 樹,並根據它創建實際的 DOM 節點。這一步作為一個響應式效果進行,所以它會跟踪所有使用的響應式依賴。
  3. 修補(patch):當掛載期間使用的依賴發生變化時,效果會重新運行。這次會創建一個新的、更新的虛擬 DOM 樹。運行時渲染器遍歷新樹,將其與舊樹進行比較,並應用必要的更新到實際 DOM。

模板與渲染函數 - Templates vs. Render Functions

Vue 模板被編譯成虛擬 DOM 渲染函數。Vue 也提供了 API,允許我們跳過模板編譯步驟,直接編寫渲染函數。渲染函數在處理高度動態的邏輯時比模板更靈活,因為您可以使用 JavaScript 的全部功能來操作 vnode。

那麼,為什麼 Vue 默認推薦使用模板呢?這有多個原因:

  1. 模板更接近實際的 HTML。這使得重用現有的 HTML 片段、更好地應用可訪問性最佳實踐、使用 CSS 進行樣式設計,以及設計師更容易理解和修改變得更簡單。
  2. 模板由於其更確定的語法,更容易進行靜態分析。這允許 Vue 的模板編譯器應用許多編譯時優化來提高虛擬 DOM 的性能(我們會在下面討論)。

實際應用中,模板對於大多數用例已經足夠。渲染函數通常只在需要處理高度動態渲染邏輯的可重用元件中使用。渲染函數的使用在 渲染函數與 JSX 中有更詳細的討論。

編譯器告知的虛擬 DOM - Compiler-Informed Virtual DOM

React 和大多數其他虛擬 DOM 的實現都是純運行時的:協調算法無法對即將到來的虛擬 DOM 樹做出任何假設,所以它必須完全遍歷樹並對每個 vnode 的屬性進行差異比較以確保正確性。此外,即使樹的一部分從未改變,每次重新渲染時也總是為它們創建新的 vnode,導致不必要的內存壓力。這是對虛擬 DOM 最常見的批評之一:某種程度上的強力協調過程在效率上做出了妥協,以換取聲明性和正確性。

但不必如此。在 Vue 中,框架控制著編譯器和運行時。這使我們能夠實現許多僅僅是緊密耦合的渲染器才能利用的編譯時優化。編譯器可以靜態分析模板,並在生成的代碼中留下提示,以便運行時可以盡可能地採取快捷方式。同時,我們仍然保留了用戶在邊緣情況下降級到渲染函數層的能力。我們稱這種混合方法為 編譯器告知的虛擬 DOM。

下面,我們將討論 Vue 模板編譯器為提高虛擬 DOM 運行時性能所做的一些主要優化。

靜態提升 - Static Hoisting

模板中通常會有一些部分不包含任何動態綁定:

<div>
<div>foo</div> <!-- 提升 -->
<div>bar</div> <!-- 提升 -->
<div>{{ dynamic }}</div>
</div>

Inspect in Template Explorer

foo 和 bar div 是靜態的,每次重新渲染時重新創建 vnode 並對它們進行比較是沒有必要的。Vue 編譯器會自動將它們的 vnode 創建調用提升到渲染函數之外,並在每次渲染時重用相同的 vnode。渲染器還能夠在發現舊 vnode 和新 vnode 是同一個時完全跳過它們的比較。

此外,當有足夠多的連續靜態元素時,它們會被壓縮成一個包含所有這些節點的純 HTML 字符串的“靜態 vnode”(示例)。這些靜態 vnode 通過直接設置 innerHTML 來掛載。它們還在初始掛載時緩存其對應的 DOM 節點——如果相同的內容在應用程序的其他地方重用,會使用原生的 cloneNode() 創建新 DOM 節點,這是非常高效的。

修補標誌 - Patch Flags

對於具有動態綁定的單個元素,我們還可以在編譯時推斷出很多信息:

<!-- 只有 class 綁定 -->
<div :class="{ active }"></div>

<!-- 只有 id 和 value 綁定 -->
<input :id="id" :value="value">

<!-- 只有文本子元素 -->
<div>{{ dynamic }}</div>

Inspect in Template Explorer

在為這些元素生成渲染函數代碼時,Vue 會直接在 vnode 創建調用中編碼每個元素需要的更新類型:

createElementVNode("div", {
class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)

最後一個參數 2 是修補標誌。一個元素可以有多個修補標誌,這些標誌將合併成一個數字。運行時渲染器可以使用位運算檢查這些標誌來確定是否需要做某些工作:

if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
// 更新元素的 class
}

位運算檢查非常快。有了修補標誌,Vue 可以在更新具有動態綁定的元素時完成最少量的工作。

Vue 還編碼了 vnode 擁有的子元素類型。例如,一個具有多個根節點的模板表示為一個片段。在大多數情況下,我們可以確定這些根節點的順序永遠不會改變,所以這些信息也可以作為修補標誌提供給運行時:

export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* 子元素 */
], 64 /* STABLE_FRAGMENT */))
}

執行階段因此可以完全跳過根節點片段的子節點順序對齊過程。

樹狀展平 (Tree Flattening)​

再看一遍前面例子生成的代碼,你會注意到返回的虛擬 DOM 樹的根是通過一個特殊的 createElementBlock() 調用創建的:

export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}

概念上,“塊”(block)是模板的一部分,具有穩定的內部結構。在這個例子中,整個模板只有一個塊,因為它不包含像 v-ifv-for 這樣的結構性指令。

每個塊會跟踪所有具有補丁標誌的子節點(不僅僅是直接子節點)。例如:

<div> <!-- 根塊 -->
<div>...</div> <!-- 不跟踪 -->
<div :id="id"></div> <!-- 跟踪 -->
<div> <!-- 不跟踪 -->
<div>{{ bar }}</div> <!-- 跟踪 -->
</div>
</div>

結果是一個扁平的數組,只包含動態的子節點:

div (根塊)
- div 帶有 :id 綁定
- div 帶有 {{ bar }} 綁定

當這個組件需要重新渲染時,它只需要遍歷這個扁平化的樹,而不是整棵樹。這被稱為樹狀展平 (Tree Flattening),它大大減少了虛擬 DOM 對齊過程中需要遍歷的節點數量。模板中的任何靜態部分都會被有效地跳過。

v-ifv-for 指令會創建新的塊節點:

<div> <!-- 根塊 -->
<div>
<div v-if> <!-- if-->
...
</div>
</div>
</div>

子塊會被跟踪在父塊的動態子節點數組中。這樣保持了父塊的穩定結構。

對 SSR Hydration 的影響​ - Impact on SSR Hydration

補丁標誌和樹狀展平也大大改善了 Vue 的 SSR Hydration 性能:

  • 單個元素的 Hydration 可以根據相應的 vnode 的補丁標誌進行快速處理。
  • 在 Hydration 過程中只需遍歷塊節點及其動態子節點,有效地在模板級別實現了部分 Hydration。
這一篇蠻不錯的耶~終於對虛擬DOM有一些概念了
了解它渲染機制感覺也很有趣~
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.