虛擬 DOM 節點
什麼是虛擬 DOM
虛擬 DOM 樹是一種描述 DOM 樹的 JavaScript 資料結構。它由巢狀的虛擬 DOM 節點組成,也稱為 vnodes。
第一次渲染虛擬 DOM 樹時,它會被用作藍圖,以建立一個與之結構匹配的 DOM 樹。
通常,虛擬 DOM 樹會在每次渲染週期中重新建立,這通常發生在響應事件處理器或資料變更時。 Mithril.js 會將 vnode 樹與其先前的版本進行 差異比對,並且僅修改發生變更的 DOM 元素。
如此頻繁地重新建立 vnode 可能看起來很浪費,但事實證明,現代 JavaScript 引擎可以在不到一毫秒的時間內創建數十萬個物件。另一方面,修改 DOM 的成本比創建 vnode 高出幾個數量級。
因此,Mithril.js 使用複雜且高度優化的虛擬 DOM 差異比對演算法,以最大程度地減少 DOM 更新的次數。 Mithril.js 也 生成精心設計的 vnode 資料結構,這些結構由 JavaScript 引擎編譯,以實現接近原生的資料結構存取效能。此外,Mithril.js 還積極優化建立 vnode 的函式。
Mithril.js 不遺餘力地支援一種渲染模型,此模型在每次渲染時重新建立整個虛擬 DOM 樹,目的是為了提供聲明式的 立即模式 API,這是一種渲染風格,可以極大地簡化 UI 複雜性的管理。
為了說明立即模式為何如此重要,請考慮 DOM API 與 HTML。 DOM API 是一種指令式的 保留模式 API,需要 1. 寫出精確的指令以程序化地組裝 DOM 樹,以及 2. 寫出其他指令以更新該樹。 DOM API 的命令式性質意味著您有很多機會可以微調您的程式碼,但也意味著您有更多機會引入錯誤,並且也更有可能使程式碼難以理解。
相比之下,HTML 更接近立即模式渲染系統。使用 HTML,您可以更自然和可讀的方式編寫 DOM 樹,而無需擔心忘記將子節點附加到父節點,或在渲染極深的樹時遇到堆疊溢位等問題。
虛擬 DOM 比 HTML 更進一步,它允許您編寫 動態 DOM 樹,而無需手動編寫多組 DOM API 呼叫,便可有效地將 UI 與任意資料變更同步。
基礎
虛擬 DOM 節點,或 vnodes,是代表 DOM 元素(或 DOM 的一部分)的 JavaScript 物件。 Mithril.js 的虛擬 DOM 引擎使用 vnode 樹來生成 DOM 樹。
Vnode 是透過 m()
hyperscript 實用工具建立的:
m('div', { id: 'test' }, 'hello');
Hyperscript 也可以使用 組件:
// 定義一個組件
var ExampleComponent = {
view: function (vnode) {
return m('div', vnode.attrs, ['Hello ', vnode.children]);
},
};
// 使用該組件
m(ExampleComponent, { style: 'color:red;' }, 'world');
// 等效的 HTML 為:
// <div style="color:red;">Hello world</div>
結構
虛擬 DOM 節點,或 vnodes,是代表元素(或 DOM 的一部分)的 JavaScript 物件,並具有以下屬性:
屬性 | 類型 | 描述 |
---|---|---|
tag | String|Object | DOM 元素的 nodeName 。如果 vnode 是一個片段,則它也可能是字串 [ ;如果它是一個文字 vnode,則為 # ;如果它是一個受信任的 HTML vnode,則為 < 。此外,它可能是一個組件。 |
key | String? | 用於將 DOM 元素對應到資料陣列中相應項目的值。 |
attrs | Object? | DOM 屬性、事件、屬性 和 生命週期方法 的雜湊表。 |
children | (Array|String|Number|Boolean)? | 在大多數 vnode 類型中,children 屬性是一個 vnode 陣列。對於文字和受信任的 HTML vnode,children 屬性是一個字串、數字或布林值。 |
text | (String|Number|Boolean)? | 如果 vnode 包含一個文字節點作為其唯一的子節點,則使用此屬性而非 children 。這樣做是為了提高效能。元件 vnode 永遠不會使用 text 屬性,即使它們有一個文字節點作為其唯一的子節點。 |
dom | Element? | 指向與 vnode 對應的元素。此屬性在 oninit 生命週期方法中是 undefined 。在片段和受信任的 HTML vnode 中,dom 指向範圍中的第一個元素。 |
domSize | Number? | 僅在片段和受信任的 HTML vnode 中設定此屬性,並且在所有其他 vnode 類型中為 undefined 。它定義了 vnode 代表的 DOM 元素的數量(從 dom 屬性引用的元素開始)。 |
state | Object? | 在重繪之間持續存在的物件。它由核心引擎在需要時提供。在 POJO 組件 vnode 中,state 從組件物件/類別以原型方式繼承。在類別組件 vnode 中,它是類別的實例。在閉包組件中,它是閉包傳回的物件。 |
events | Object? | 在重繪之間持續存在的物件,儲存事件處理器,以便可以使用 DOM API 移除它們。如果未定義事件處理器,則 events 屬性為 undefined 。此屬性僅供 Mithril.js 內部使用,請勿使用或修改它。 |
instance | Object? | 對於組件,view 返回的值的儲存位置。此屬性僅供 Mithril.js 內部使用,請勿使用或修改它。 |
Vnode 類型
vnode 的 tag
屬性決定其類型。有五種 vnode 類型:
Vnode 類型 | 範例 | 描述 |
---|---|---|
元素 | {tag: "div"} | 代表一個 DOM 元素。 |
片段 | {tag: "[", children: []} | 代表一個 DOM 元素列表,其父 DOM 元素也可能包含不在片段中的其他元素。使用 m() 輔助函式時,只能透過將陣列嵌套到 m() 的 children 參數中來建立片段 vnode。 m("[") 不會建立有效的 vnode。 |
文字 | {tag: "#", children: ""} | 代表一個 DOM 文字節點。 |
受信任的 HTML | {tag: "<", children: "<br>"} | 代表來自 HTML 字串的 DOM 元素列表。 |
組件 | {tag: ExampleComponent} | 如果 tag 是一個具有 view 方法的 JavaScript 物件,則 vnode 代表透過渲染該組件產生的 DOM。 |
虛擬 DOM 樹中的所有內容都是 vnode,包括文字。 m()
實用程式會自動正規化其 children
參數,並將字串轉換為文字 vnode,並將嵌套陣列轉換為片段 vnode。
只有元素標籤名稱和組件可以作為 m()
函式的第一個參數。換句話說,[
、#
和 <
不是 m()
的有效 selector
參數。受信任的 HTML vnode 可以透過 m.trust()
來建立。
單態類別
Mithril.js 使用 mithril/render/vnode
模組來生成所有 vnode。這確保了現代 JavaScript 引擎可以透過始終將 vnode 編譯為相同的隱藏類別來優化虛擬 DOM 差異比對。
在建立發出 vnode 的程式庫時,您應該使用此模組,而不是編寫純 JavaScript 物件,以確保高水準的渲染效能。
避免反模式
避免重複使用 vnode
Vnode 應該代表 DOM 在特定時間點的狀態。 Mithril.js 的渲染引擎假設重複使用的 vnode 未更改,因此修改先前渲染中使用的 vnode 將導致未定義的行為。
可以就地重複使用 vnode 以防止差異,但最好使用 onbeforeupdate
。
避免透過屬性將模型資料直接傳遞給組件
key
屬性可能會以與 Mithril.js 的鍵邏輯衝突的方式出現在您的資料模型中,並且您的模型本身可能是一個可變的實例,其方法與生命週期鉤子(如 onupdate
或 onremove
)共享一個名稱。例如,模型可能會使用 key
屬性來表示可自訂的顏色鍵值。當此屬性變更時,可能會導致組件接收錯誤的資料、意外地變更位置或其他意外的、不希望的行為。相反,將其作為屬性傳遞,以便 Mithril.js 不會錯誤地解釋它(並且您仍然可以稍後潛在地變更它或在其上呼叫原型方法)。
// 數據模型
var users = [
{ id: 1, name: 'John', key: 'red' },
{ id: 2, name: 'Mary', key: 'blue' },
];
// 稍後...
users[0].key = 'yellow';
// 避免
users.map(function (user) {
// John 的組件將被銷毀並重新建立
return m(UserComponent, user);
});
// 偏好
users.map(function (user) {
// 鍵值被明確提取:資料模型被賦予自己的屬性
return m(UserComponent, { key: user.id, model: user });
});
避免在 view 方法中使用語句
view 方法中的 JavaScript 語句通常需要改變 HTML 樹的自然嵌套結構,從而使程式碼更加冗長且可讀性較差。
// 避免
var BadListComponent = {
view: function (vnode) {
var list = [];
for (var i = 0; i < vnode.attrs.items.length; i++) {
list.push(m('li', vnode.attrs.items[i]));
}
return m('ul', list);
},
};
相反,應更喜歡使用 JavaScript 表達式,例如用於條件渲染的三元運算符和用於類似列表結構的陣列。
// 偏好
var BetterListComponent = {
view: function (vnode) {
return m(
'ul',
vnode.attrs.items.map(function (item) {
return m('li', item);
})
);
},
};