渲染機制
Vue 如何將模板轉換為實際的 DOM 節點?Vue 如何高效地更新這些 DOM 節點?在這裡,我們將透過深入瞭解 Vue 的內部渲染機制來嘗試解答這些問題。
虛擬 DOM
你可能已經聽說過“虛擬 DOM”這個術語,Vue 的渲染系統就是基於這個概念。
虛擬 DOM(VDOM)是一種程式設計概念,其中在記憶體中保持 UI 的理想或“虛擬”表示,並與“真實”DOM 保持同步。這個概念由 React 領先提出,並被許多其他框架採用,包括 Vue。
虛擬 DOM 更像是一種模式,而不是一項具體的技術,因此沒有一種官方的實現。我們可以用一個簡單的例子來說明這個概念
js
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* more vnodes */
]
}
在這裡,vnode
是一個普通的 JavaScript 物件(一個“虛擬節點”),代表一個 <div>
元素。它包含建立實際元素所需的所有資訊。它還包含更多的子 vnode,這使得它成為虛擬 DOM 樹的根節點。
執行時渲染器可以遍歷虛擬 DOM 樹,並從中構建一個實際的 DOM 樹。這個過程被稱為 掛載。
如果我們有兩個虛擬DOM樹的副本,渲染器也可以遍歷並比較這兩個樹,找出差異,並將這些更改應用到實際的DOM上。這個過程被稱為補丁,也稱為“差異”或“協調”。
虛擬DOM的主要好處是它為開發者提供了以宣告性方式程式化建立、檢查和組合所需UI結構的能力,同時將直接DOM操作留給渲染器。
渲染管道
從高層次來說,當Vue元件掛載時會發生這樣的事情
編譯:Vue模板被編譯成返回虛擬DOM樹的渲染函式:函式。這一步可以透過構建步驟提前完成,或者透過使用執行時編譯器即時完成。
掛載:執行時渲染器呼叫渲染函式,遍歷返回的虛擬DOM樹,並根據它建立實際的DOM節點。這一步作為一個響應式效果執行,因此它會跟蹤所有使用的響應式依賴項。
補丁:當掛載期間使用的依賴項發生變化時,效果會重新執行。這次,建立了一個新的、更新的虛擬DOM樹。執行時渲染器遍歷新樹,將其與舊樹進行比較,並將必要的更新應用到實際的DOM上。
模板與渲染函式
Vue模板被編譯成虛擬DOM渲染函式。Vue還提供了API,允許我們跳過模板編譯步驟並直接編寫渲染函式。當處理高度動態的邏輯時,渲染函式比模板更靈活,因為你可以使用JavaScript的全部功能來處理vnode。
那麼為什麼Vue預設推薦使用模板?有幾個原因
模板更接近實際的HTML。這使得它更容易重用現有的HTML片段,應用無障礙性最佳實踐,使用CSS進行樣式化,以及讓設計師理解並修改。
由於它們的語法更具確定性,模板更容易進行靜態分析。這使得Vue的模板編譯器可以對虛擬DOM(我們將在下面討論)應用許多編譯時最佳化以提高效能。
在實踐中,模板對於應用中的大多數用例來說都是足夠的。渲染函式通常只在需要處理高度動態渲染邏輯的通用元件中使用。渲染函式的使用在渲染函式 & JSX中更詳細地討論。
編譯器知情虛擬DOM
React和大多數其他虛擬DOM實現中的虛擬DOM實現完全是執行時:協調演算法無法對傳入的虛擬DOM樹做出任何假設,因此它必須完全遍歷樹並比較每個vnode的props以確保正確性。此外,即使樹的一部分從未改變,在每次重新渲染時也會為它們建立新的vnode,這會導致不必要的記憶體壓力。這是虛擬DOM最被批評的方面之一:這種有點蠻力的協調過程以犧牲效率為代價換取了宣告性和正確性。
但不必如此。在Vue中,框架控制編譯器和執行時。這使我們能夠實施許多隻有緊密耦合的渲染器才能利用的編譯時最佳化。編譯器可以靜態分析模板,並在生成的程式碼中留下提示,以便執行時可以在可能的情況下采取捷徑。同時,我們仍然保留了使用者在邊緣情況中下降到渲染函式層的直接控制能力。我們稱這種混合方法為編譯器知情虛擬DOM。
下面,我們將討論Vue模板編譯器為了提高虛擬DOM的執行時效能所做的一些主要最佳化。
靜態提升
模板中經常會包含一些不包含任何動態繫結的部分
template
<div>
<div>foo</div> <!-- hoisted -->
<div>bar</div> <!-- hoisted -->
<div>{{ dynamic }}</div>
</div>
《foo》和《bar》的div是靜態的 - 在每次重新渲染時重新建立vnode和比較它們是不必要的。Vue編譯器會自動將vnode建立呼叫提升出渲染函式,並在每次渲染時重用相同的vnode。當渲染器發現舊vnode和新vnode是同一個時,它還能完全跳過它們的比較。
此外,當有足夠的連續靜態元素時,它們將被壓縮成一個包含所有這些節點純HTML字串的“靜態vnode”(示例)。這些靜態vnode透過直接設定innerHTML來掛載。它們還快取了初始掛載時的相應DOM節點 - 如果同一內容在應用程式的其它地方被重用,則會建立新的DOM節點,使用原生的cloneNode()
,這非常高效。
補丁標誌
對於單個具有動態繫結的元素,我們也可以在編譯時從它那裡推斷出很多資訊
template
<!-- class binding only -->
<div :class="{ active }"></div>
<!-- id and value bindings only -->
<input :id="id" :value="value">
<!-- text children only -->
<div>{{ dynamic }}</div>
當為這些元素生成渲染函式程式碼時,Vue會直接在vnode建立呼叫中編碼每個元素所需的更新型別
js
createElementVNode("div", {
class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)
最後一個引數2
是一個補丁標誌。一個元素可以有多個補丁標誌,這些標誌將合併成一個數字。執行時渲染器可以透過位操作來檢查標誌,以確定是否需要執行某些工作
js
if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
// update the element's class
}
位操作非常快。有了補丁標誌,Vue能夠在更新具有動態繫結的元素時做最少的必要工作。
Vue還會編碼vnode的子節點型別。例如,具有多個根節點的模板表示為片段。在大多數情況下,我們知道這些根節點的順序永遠不會改變,因此這些資訊也可以作為補丁標誌提供給執行時
js
export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}
因此,執行時可以完全跳過根片段的子節點順序的協調。
樹扁平化
再次檢視上一個示例生成的程式碼,你會注意到返回的虛擬DOM樹的根是透過特殊的createElementBlock()
呼叫建立的
js
export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}
在概念上,“塊”是模板中具有穩定內部結構的部分。在這種情況下,整個模板只有一個塊,因為它不包含任何結構指令,如v-if
和v-for
。
每個塊跟蹤任何具有補丁標誌的後代節點(不僅是直接子節點),例如
template
<div> <!-- root block -->
<div>...</div> <!-- not tracked -->
<div :id="id"></div> <!-- tracked -->
<div> <!-- not tracked -->
<div>{{ bar }}</div> <!-- tracked -->
</div>
</div>
結果是隻包含動態後代節點的扁平化陣列
div (block root)
- div with :id binding
- div with {{ bar }} binding
當此元件需要重新渲染時,它只需要遍歷扁平化的樹,而不是完整的樹。這被稱為 樹扁平化,它在虛擬DOM的重新同步過程中大大減少了需要遍歷的節點數量。模板中的任何靜態部分都有效地被跳過。
v-if
和 v-for
指令將建立新的塊節點
template
<div> <!-- root block -->
<div>
<div v-if> <!-- if block -->
...
</div>
</div>
</div>
子塊在父塊動態子代陣列中被跟蹤。這保留了父塊的穩定結構。
對SSR同步的影響
補丁標誌和樹扁平化也極大地提高了Vue的 SSR同步 效能
單元素同步可以根據相應的vnode的補丁標誌採取快速路徑。
在同步過程中,只需要遍歷塊節點及其動態子代,從而在模板級別上實現部分同步。