Rendering Mechanism 一直不懂這是什麼!
希望多了解一點~應該對框架會更熟悉吧www
Vue 如何將模板轉換為實際的 DOM 節點?
Vue 如何高效地更新這些 DOM 節點?
我們將深入探討 Vue 的內部渲染機制,試圖解答這些問題。
您可能聽說過 "虛擬 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 操作留給渲染器處理。
在高層次上,這是 Vue 元件掛載時發生的事情:
Vue 模板被編譯成虛擬 DOM 渲染函數。Vue 也提供了 API,允許我們跳過模板編譯步驟,直接編寫渲染函數。渲染函數在處理高度動態的邏輯時比模板更靈活,因為您可以使用 JavaScript 的全部功能來操作 vnode。
那麼,為什麼 Vue 默認推薦使用模板呢?這有多個原因:
實際應用中,模板對於大多數用例已經足夠。渲染函數通常只在需要處理高度動態渲染邏輯的可重用元件中使用。渲染函數的使用在 渲染函數與 JSX 中有更詳細的討論。
React 和大多數其他虛擬 DOM 的實現都是純運行時的:協調算法無法對即將到來的虛擬 DOM 樹做出任何假設,所以它必須完全遍歷樹並對每個 vnode 的屬性進行差異比較以確保正確性。此外,即使樹的一部分從未改變,每次重新渲染時也總是為它們創建新的 vnode,導致不必要的內存壓力。這是對虛擬 DOM 最常見的批評之一:某種程度上的強力協調過程在效率上做出了妥協,以換取聲明性和正確性。
但不必如此。在 Vue 中,框架控制著編譯器和運行時。這使我們能夠實現許多僅僅是緊密耦合的渲染器才能利用的編譯時優化。編譯器可以靜態分析模板,並在生成的代碼中留下提示,以便運行時可以盡可能地採取快捷方式。同時,我們仍然保留了用戶在邊緣情況下降級到渲染函數層的能力。我們稱這種混合方法為 編譯器告知的虛擬 DOM。
下面,我們將討論 Vue 模板編譯器為提高虛擬 DOM 運行時性能所做的一些主要優化。
模板中通常會有一些部分不包含任何動態綁定:
<div>
<div>foo</div> <!-- 提升 -->
<div>bar</div> <!-- 提升 -->
<div>{{ dynamic }}</div>
</div>
foo 和 bar div 是靜態的,每次重新渲染時重新創建 vnode 並對它們進行比較是沒有必要的。Vue 編譯器會自動將它們的 vnode 創建調用提升
到渲染函數之外,並在每次渲染時重用相同的 vnode。渲染器還能夠在發現舊 vnode 和新 vnode 是同一個時完全跳過它們的比較。
此外,當有足夠多的連續靜態元素時,它們會被壓縮成一個包含所有這些節點的純 HTML 字符串的“靜態 vnode”(示例)。這些靜態 vnode 通過直接設置 innerHTML 來掛載。它們還在初始掛載時緩存其對應的 DOM 節點——如果相同的內容在應用程序的其他地方重用,會使用原生的 cloneNode() 創建新 DOM 節點,這是非常高效的。
對於具有動態綁定的單個元素,我們還可以在編譯時推斷出很多信息:
<!-- 只有 class 綁定 -->
<div :class="{ active }"></div>
<!-- 只有 id 和 value 綁定 -->
<input :id="id" :value="value">
<!-- 只有文本子元素 -->
<div>{{ dynamic }}</div>
在為這些元素生成渲染函數代碼時,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 */))
}
執行階段因此可以完全跳過根節點片段的子節點順序對齊過程。
再看一遍前面例子生成的代碼,你會注意到返回的虛擬 DOM 樹的根是通過一個特殊的 createElementBlock()
調用創建的:
export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}
概念上,“塊”(block)是模板的一部分,具有穩定的內部結構。在這個例子中,整個模板只有一個塊,因為它不包含像 v-if
和 v-for
這樣的結構性指令。
每個塊會跟踪所有具有補丁標誌的子節點(不僅僅是直接子節點)。例如:
<div> <!-- 根塊 -->
<div>...</div> <!-- 不跟踪 -->
<div :id="id"></div> <!-- 跟踪 -->
<div> <!-- 不跟踪 -->
<div>{{ bar }}</div> <!-- 跟踪 -->
</div>
</div>
結果是一個扁平的數組,只包含動態的子節點:
div (根塊)
- div 帶有 :id 綁定
- div 帶有 {{ bar }} 綁定
當這個組件需要重新渲染時,它只需要遍歷這個扁平化的樹,而不是整棵樹。這被稱為樹狀展平 (Tree Flattening),它大大減少了虛擬 DOM 對齊過程中需要遍歷的節點數量。模板中的任何靜態部分都會被有效地跳過。
v-if
和 v-for
指令會創建新的塊節點:
<div> <!-- 根塊 -->
<div>
<div v-if> <!-- if 塊 -->
...
</div>
</div>
</div>
子塊會被跟踪在父塊的動態子節點數組中。這樣保持了父塊的穩定結構。
補丁標誌和樹狀展平也大大改善了 Vue 的 SSR Hydration 性能:
這一篇蠻不錯的耶~終於對虛擬DOM有一些概念了
了解它渲染機制感覺也很有趣~