虚拟 DOM
虚拟 DOM
虚拟 DOM 树是一个 JavaScript 数据结构,用于描述 DOM 树。它由嵌套的虚拟 DOM 节点组成,也称为 vnodes(虚拟节点)。
首次渲染虚拟 DOM 树时,它被用作蓝图,创建一个与其结构匹配的 DOM 树。
通常,虚拟 DOM 树会在每个渲染周期中重新创建,这通常是响应事件处理程序或数据更改而发生的。Mithril.js 将 vnode 树与其先前版本进行 diff,仅在发生更改的地方修改 DOM 元素。
频繁地重新创建 vnodes 似乎效率不高,但现代 JavaScript 引擎可以在不到一毫秒的时间内创建数十万个对象。另一方面,修改 DOM 的开销比创建 vnodes 高出几个数量级。
Mithril.js 使用一种复杂且高度优化的虚拟 DOM 差异比较算法,以最大限度地减少 DOM 更新的数量。Mithril.js 还生成精心设计的 vnode 数据结构,这些数据结构由 JavaScript 引擎编译,以实现接近原生数据结构的访问性能。此外,Mithril.js 还积极优化创建 vnodes 的函数。
Mithril.js 致力于支持一种渲染模型,该模型在每次渲染时重新创建整个虚拟 DOM 树。原因在于它要提供一个声明式的立即模式 API,这种渲染风格可以极大地简化 UI 复杂性的管理。
为了说明立即模式的重要性,请考虑 DOM API 和 HTML。DOM API 是一种命令式的 retained mode API,它需要:1. 编写精确的指令以程序化地组装 DOM 树;2. 编写其他指令来更新该树。DOM API 的命令式性质意味着您有很多机会来微调您的代码,但也意味着您有更多机会引入错误,并且更有可能使代码难以理解。
相比之下,HTML 更接近于立即模式的渲染系统。使用 HTML,您可以以一种更自然和可读的方式编写 DOM 树,而无需担心忘记将子节点附加到父节点,或者在渲染极深树时遇到堆栈溢出等问题。
虚拟 DOM 比 HTML 更进一步,它允许您编写 动态 DOM 树,无需手动编写多组 DOM API 调用,即可有效地将 UI 与任意数据更改同步。
基础
虚拟 DOM 节点,或 vnodes,是 JavaScript 对象,用于表示 DOM 元素(或 DOM 的一部分)。Mithril.js 的虚拟 DOM 引擎使用 vnodes 树来生成 DOM 树。
Vnodes 通过 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,是 JavaScript 对象,用于表示元素(或 DOM 的一部分),并具有以下属性:
属性 | 类型 | 描述 |
---|---|---|
tag | String|Object | DOM 元素的 nodeName 。如果 vnode 是一个片段,则它也可能是字符串 [ ;如果它是一个文本 vnode,则为 # ;如果它是一个受信任的 HTML vnode,则为 < 。此外,tag 也可以是一个组件。 |
key | String? | 用于将 DOM 元素映射到数据数组中对应项的值。 |
attrs | Object? | DOM 属性、事件、属性和生命周期方法的哈希映射。 |
children | (Array|String|Number|Boolean)? | 在大多数 vnode 类型中,children 属性是一个 vnode 数组。对于文本和受信任的 HTML vnodes,children 属性是一个字符串、一个数字或一个布尔值。 |
text | (String|Number|Boolean)? | 如果 vnode 仅包含一个文本节点作为其唯一子节点,则使用此属性代替 children 。这样做是为了提高性能。即使组件 vnodes 有一个文本节点作为其唯一子节点,也永远不会使用 text 属性。 |
dom | Element? | 指向与 vnode 对应的 DOM 元素。在 oninit 生命周期方法中,此属性是 undefined 。在片段和受信任的 HTML vnodes 中,dom 指向范围内的第一个元素。 |
domSize | Number? | 仅在片段和受信任的 HTML vnodes 中设置此属性,并且在所有其他 vnode 类型中,它为 undefined 。它定义了 vnode 所表示的 DOM 元素的数量(从 dom 属性引用的元素开始)。 |
state | Object? | 一个在重绘之间持久存在的对象,由核心引擎在需要时提供。在 POJO 组件 vnodes 中,state 从组件对象/类以原型方式继承。在类组件 vnodes 中,它是该类的一个实例。在闭包组件中,它是闭包返回的对象。 |
events | Object? | 一个在重绘之间持久存在的对象,用于存储事件处理程序,以便可以使用 DOM API 删除它们。如果未定义事件处理程序,则 events 属性是 undefined 。此属性仅供 Mithril.js 内部使用,请勿使用或修改它。 |
instance | Object? | 对于组件,view 返回的值的存储位置。此属性仅供 Mithril.js 内部使用,请勿使用或修改它。 |
Vnode 类型
tag
属性确定 vnode 的类型。有五种 vnode 类型:
Vnode 类型 | 示例 | 描述 |
---|---|---|
元素 | {tag: "div"} | 表示一个 DOM 元素。 |
片段 | {tag: "[", children: []} | 表示一个 DOM 元素列表,其父 DOM 元素也可能包含不在该片段中的其他元素。使用 m() 辅助函数时,只能通过将数组嵌套到 m() 的 children 参数中来创建片段 vnodes。m("[") 不会创建有效的 vnode。 |
文本 | {tag: "#", children: ""} | 表示一个 DOM 文本节点。 |
受信任的 HTML | {tag: "<", children: "<br>"} | 表示来自 HTML 字符串的 DOM 元素列表。 |
组件 | {tag: ExampleComponent} | 如果 tag 是一个具有 view 方法的 JavaScript 对象,则 vnode 表示通过渲染组件生成的 DOM。 |
虚拟 DOM 树中的所有内容都是一个 vnode,包括文本。m()
实用程序会自动规范化其 children
参数,并将字符串转换为文本 vnodes,并将嵌套数组转换为片段 vnodes。
只有元素标签名称和组件可以是 m()
函数的第一个参数。换句话说,[
、#
和 <
不是 m()
的有效 selector
参数。可以通过 m.trust()
创建受信任的 HTML vnodes。
单态
Mithril.js 使用 mithril/render/vnode
模块来生成所有 vnodes。这确保了现代 JavaScript 引擎可以通过始终将 vnodes 编译为相同的隐藏类来优化虚拟 dom diff。
在创建发出 vnodes 的库时,为了确保高水平的渲染性能,您应该使用此模块,而不是编写裸 JavaScript 对象。
反模式
重用 vnodes
Vnodes 应该表示某个时间点的 DOM 状态。Mithril.js 的渲染引擎假定重用的 vnode 未更改,因此修改在先前渲染中使用过的 vnode 将导致未定义的行为。
可以就地重用 vnodes 以防止 diff,但建议使用 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);
})
);
},
};