m(selector, attributes, children)
描述
表示 Mithril.js 视图中的 HTML 元素。
m('div.foo', { style: { color: 'red' } }, 'hello');
// 渲染为以下 HTML:
// <div class="foo" style="color: red">hello</div>
你也可以使用类似 HTML 的语法,称为 JSX,并使用 Babel 将其转换为等效的 hyperscript 调用。以下代码与上述代码等效。
<div class="foo" style="color: red">
hello
</div>
签名
vnode = m(selector, attrs, children)
参数 | 类型 | 必需 | 描述 |
---|---|---|---|
selector | String|Object|Function | 是 | CSS 选择器或 组件 |
attrs | Object | 否 | HTML 属性或 DOM 元素属性 |
children | Array<Vnode>|String|Number|Boolean | 否 | 子 vnode,可以写成 splat 参数 |
返回值 | Vnode | 一个 vnode |
工作原理
Mithril.js 提供了一个 hyperscript 函数 m()
,允许使用 JavaScript 语法来描述任何 HTML 结构。它接受一个 selector
字符串(必需),一个 attrs
对象(可选)和一个 children
数组(可选)。
m('div', { id: 'box' }, 'hello');
// 渲染为以下 HTML:
// <div id="box">hello</div>
m()
函数实际上并不返回一个 DOM 元素。相反,它返回一个 虚拟 DOM 节点,也称为 vnode。vnode 是一个 JavaScript 对象,表示要创建的 DOM 元素。
// 一个 vnode
var vnode = {
tag: 'div',
attrs: { id: 'box' },
children: [
/*...*/
],
};
要将 vnode 转换为实际的 DOM 元素,请使用 m.render()
函数:
m.render(document.body, m('br')); // 在 <body> 中放入一个 <br>
多次调用 m.render()
不会 每次都从头开始重新创建 DOM 树。相反,每次调用仅在必要时根据传入的虚拟 DOM 树,对实际 DOM 树进行必要的修改。这种设计是合理的,因为从头开始重新创建 DOM 的代价很高,并且会导致诸如失去输入焦点之类的问题。相比之下,仅在必要时更新 DOM 速度更快,并且更容易维护处理多个用户场景的复杂 UI。
灵活性
m()
函数既是 多态的 又是 参数可变的。 换句话说,它对输入参数的要求非常灵活:
// 简单的标签
m('div'); // <div></div>
// 属性和子元素是可选的
m('a', { id: 'b' }); // <a id="b"></a>
m('span', 'hello'); // <span>hello</span>
// 带有子节点的标签
m('ul', [
// <ul>
m('li', 'hello'), // <li>hello</li>
m('li', 'world'), // <li>world</li>
]); // </ul>
// 数组是可选的
m(
'ul', // <ul>
m('li', 'hello'), // <li>hello</li>
m('li', 'world') // <li>world</li>
); // </ul>
CSS 选择器
m()
的第一个参数可以是任何可以描述 HTML 元素的 CSS 选择器。它接受 #
(id)、.
(class) 和 []
(attribute) 语法的任何有效的 CSS 组合。
m('div#hello');
// <div id="hello"></div>
m('section.container');
// <section class="container"></section>
m('input[type=text][placeholder=Name]');
// <input type="text" placeholder="Name" />
m("a#exit.external[href='https://example.com']", 'Leave');
// <a id="exit" class="external" href="https://example.com">Leave</a>
如果省略标签名称,Mithril.js 假定为 div
标签。
m('.box.box-bordered'); // <div class="box box-bordered"></div>
通常,建议对静态属性(即值不变的属性)使用 CSS 选择器,并为动态属性值传递一个属性对象。
var currentURL = '/';
m(
'a.link[href=/]',
{
class: currentURL === '/' ? 'selected' : '',
},
'Home'
);
// 渲染为以下 HTML:
// <a href="/" class="link selected">Home</a>
作为第二个参数传递的属性
你可以在第二个可选参数中传递属性、事件和生命周期钩子(有关详细信息,请参见下一节)。
m("button", {
class: "my-button",
onclick: function() {/* ... */},
oncreate: function() {/* ... */}
})
如果此类属性的值为 null
或 undefined
,则该属性会被忽略。
如果 m()
的第一个和第二个参数中都存在类名,则它们会按照预期合并在一起。如果第二个参数中 class 的值为 null
或 undefined
,则将其忽略。
如果同一个属性同时存在于第一个和第二个参数中,则第二个参数优先,即使它是 null
或 undefined
。
DOM 属性
Mithril.js 使用 JavaScript API 和 DOM API (setAttribute
) 来处理属性。这意味着你可以使用两种方式来访问属性。
例如,在 JavaScript API 中,readonly
属性称为 element.readOnly
(注意大写)。在 Mithril.js 中,以下所有写法都支持:
m('input', { readonly: true }); // 小写
m('input', { readOnly: true }); // 大写
m('input[readonly]');
m('input[readOnly]');
这甚至包括自定义元素。例如,你可以在 Mithril.js 中使用 A-Frame,没有问题!
m('a-scene', [
m('a-box', {
position: '-1 0.5 -3',
rotation: '0 45 0',
color: '#4CC3D9',
}),
m('a-sphere', {
position: '0 1.25 -5',
radius: '1.25',
color: '#EF2D5E',
}),
m('a-cylinder', {
position: '1 0.75 -3',
radius: '0.5',
height: '1.5',
color: '#FFC65D',
}),
m('a-plane', {
position: '0 0 -4',
rotation: '-90 0 0',
width: '4',
height: '4',
color: '#7BC8A4',
}),
m('a-sky', {
color: '#ECECEC',
}),
]);
对于自定义元素,Mithril.js 不会自动将属性字符串化,以防它们是对象、数字或某些其他非字符串值。因此,假设你有一个自定义元素 my-special-element
,它具有一个 elem.whitelist
数组 getter/setter 属性,你可以这样做,并且它会按照预期工作:
m('my-special-element', {
whitelist: [
'https://example.com',
'https://neverssl.com',
'https://google.com',
],
});
如果这些元素具有类或 ID,则简写仍然可以按预期工作。再举一个 A-Frame 示例:
// 这两个是等效的
m('a-entity#player');
m('a-entity', { id: 'player' });
请注意,所有具有特殊语义的属性,例如生命周期属性、onevent
处理程序、key
、class
和 style
,它们的处理方式与普通 HTML 元素相同。
Style 属性
Mithril.js 支持字符串和对象作为有效的 style
值。换句话说,以下所有写法都支持:
m('div', { style: 'background:red;' });
m('div', { style: { background: 'red' } });
m('div[style=background:red]');
如果使用字符串作为 style
的值,重新渲染时会覆盖元素的所有内联样式,而不仅仅是值发生改变的 CSS 规则。
你可以使用连字符 CSS 属性名称(如 background-color
)和驼峰式 DOM style
属性名称(如 backgroundColor
)。如果你的浏览器支持,你还可以定义 CSS 自定义属性。
Mithril.js 不会尝试向数字值添加单位。它只是将它们字符串化。
事件
Mithril.js 支持所有 DOM 事件的事件处理程序绑定,包括规范中未定义 on${event}
属性的事件,例如 touchstart
。
function doSomething(e) {
console.log(e);
}
m('div', { onclick: doSomething });
Mithril.js 接受函数和 EventListener 对象。因此,以下写法也有效:
var clickListener = {
handleEvent: function (e) {
console.log(e);
},
};
m('div', { onclick: clickListener });
默认情况下,当通过 hyperscript 绑定的事件触发时,这将在你的事件回调返回后触发 Mithril.js 的自动重绘(假设你使用的是 m.mount
或 m.route
而不是直接使用 m.render
)。你可以通过在其上设置 e.redraw = false
来专门为单个事件禁用自动重绘:
m('div', {
onclick: function (e) {
// 阻止自动重绘
e.redraw = false;
},
});
属性
Mithril.js 支持通过属性访问的 DOM 功能,例如 <select>
的 selectedIndex
和 value
属性。
m('select', { selectedIndex: 0 }, [
m('option', 'Option A'),
m('option', 'Option B'),
]);
组件
组件 允许你将逻辑封装到一个单元中,并像使用元素一样使用它。它们是构建大型、可扩展应用程序的基础。
组件是任何包含 view
方法的 JavaScript 对象。要使用组件,请将组件作为 m()
的第一个参数传入,而不是传入 CSS 选择器字符串。你可以通过定义属性和子元素将参数传递给组件,如下例所示。
// 定义一个组件
var Greeter = {
view: function (vnode) {
return m('div', vnode.attrs, ['Hello ', vnode.children]);
},
};
// 使用它
m(Greeter, { style: 'color:red;' }, 'world');
// 渲染为以下 HTML:
// <div style="color:red;">Hello world</div>
要了解有关组件的更多信息,请查看组件页面。
生命周期方法
Vnode 和组件可以具有生命周期方法(也称为 hooks),这些方法在 DOM 元素生命周期的各个时间点被调用。Mithril.js 支持的生命周期方法有:oninit
、oncreate
、onupdate
、onbeforeremove
、onremove
和 onbeforeupdate
。
生命周期方法的定义方式与 DOM 事件处理程序相同,但接收 vnode 作为参数,而不是 Event 对象:
function initialize(vnode) {
console.log(vnode);
}
m('div', { oninit: initialize });
Hook | 描述 |
---|---|
oninit(vnode) | 在 vnode 渲染为实际的 DOM 元素之前运行。 |
oncreate(vnode) | 在 vnode 附加到 DOM 后运行。 |
onupdate(vnode) | 每次在 DOM 元素附加到文档时发生重绘时运行。 |
onbeforeremove(vnode) | 在从文档中删除 DOM 元素之前运行。如果返回 Promise,Mithril.js 会等待 Promise 完成后才移除 DOM 元素。此方法只在从父 DOM 元素移除的元素上触发,不会在其子元素上触发。 |
onremove(vnode) | 在从文档中删除 DOM 元素之前运行。如果定义了 onbeforeremove 钩子,则在 onbeforeremove 执行完毕后调用 onremove 。此方法在从其父元素分离的元素及其所有子元素上触发。 |
onbeforeupdate(vnode, old) | 在 onupdate 之前运行,如果返回 false ,则会阻止对该元素及其所有子元素进行差异比较。 |
要了解有关生命周期方法的更多信息,请参见生命周期方法页面。
Keys
列表中的 Vnode 可以具有一个名为 key
的特殊属性,该属性可用于在生成 vnode 列表的模型数据更改时管理 DOM 元素的标识。
通常,key
应该是数据数组中对象的唯一标识符。
var users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Mary' },
];
function userInputs(users) {
return users.map(function (u) {
return m('input', { key: u.id }, u.name);
});
}
m.render(document.body, userInputs(users));
设置 key 意味着,即使 users
数组的顺序被打乱,重新渲染视图后,输入框的顺序仍然保持不变,从而保持正确的焦点和 DOM 状态。
要了解有关 key 的更多信息,请参见 key 页面。
SVG 和 MathML
Mithril.js 完全支持 SVG。Xlink 也受支持,但与 Mithril.js 的 pre-v1.0 版本不同,必须显式定义命名空间:
m('svg', [m("image[xlink:href='image.gif']")]);
MathML 也完全受支持。
使模板动态化
由于嵌套的 vnode 只是普通的 JavaScript 表达式,你可以直接使用 JavaScript 的各种特性来操作它们。
动态文本
var user = { name: 'John' };
m('.name', user.name); // <div class="name">John</div>
循环
使用 Array
方法,例如 map
来迭代数据列表。
var users = [{ name: 'John' }, { name: 'Mary' }];
m(
'ul',
users.map(function (u) {
// <ul>
return m('li', u.name); // <li>John</li>
// <li>Mary</li>
})
); // </ul>
// ES6+:
// m("ul", users.map(u =>
// m("li", u.name)
// ))
条件
使用三元运算符在视图中有条件地渲染内容。
var isError = false;
m('div', isError ? 'An error occurred' : 'Saved'); // <div>Saved</div>
你不能在 JavaScript 表达式中使用 JavaScript 语句,例如 if
或 for
。最好完全避免使用这些语句,而是专门使用上面的构造,以便保持模板的结构线性且声明式。
转换 HTML
在 Mithril.js 中,格式良好的 HTML 是有效的 JSX。只需要复制粘贴,就可以将独立生成的 HTML 文件集成到使用 JSX 的项目中。
使用 hyperscript 时,需要先将 HTML 转换为 hyperscript 语法,然后才能运行代码。为了方便起见,你可以使用 HTML-to-Mithril-template 转换器。
避免反模式
虽然 Mithril.js 很灵活,但有些代码模式是不推荐的:
避免动态选择器
不同的 DOM 元素具有不同的属性,并且通常具有不同的行为。使选择器可配置可能会导致组件实现细节的泄露。
// 避免
var BadInput = {
view: function (vnode) {
return m('div', [m('label'), m(vnode.attrs.type || 'input')]);
},
};
与其使用动态选择器,更推荐的做法是显式地编写每种可能的选择器,或者重构代码中可变的部分。
// 优先选择显式代码
var BetterInput = {
view: function (vnode) {
return m('div', [m('label', vnode.attrs.title), m('input')]);
},
};
var BetterSelect = {
view: function (vnode) {
return m('div', [m('label', vnode.attrs.title), m('select')]);
},
};
// 优先选择重构可变性
var BetterLabeledComponent = {
view: function (vnode) {
return m('div', [m('label', vnode.attrs.title), vnode.children]);
},
};
避免在视图之外创建 vnode
如果重绘时发现某个 vnode 与前一次渲染的 vnode 完全相同,Mithril.js 会跳过该 vnode 及其子树的更新。虽然这看起来像是性能优化的机会,但应避免这样做,因为它会阻止该节点树中的动态更改 - 这会导致副作用,例如下游生命周期方法无法在重绘时触发。因此,Mithril.js vnode 是不可变的:新的 vnode 与旧的 vnode 进行比较;对 vnode 的修改不会被保留。
组件文档包含更多详细信息和此反模式的示例。