Mithril m(selector, attributes, children)

m(selector, attributes, children)

说明

在 Mithril 视图中表示一个 HTML 元素:

m("div", {class: "foo"}, "hello")
// 表示 <div class="foo">hello</div>

你也可以通过 Babel 插件使用 HTML 语法

/** jsx m */
<div class="foo">hello</div>

签名

vnode = m(selector, attributes, children)
参数 类型 是否必须 描述
selector String|Object CSS 选择器或组件
attributes Object HTML 属性或元素属性
children Array|String|Number|Boolean vnodes。也可以写成解构参数
返回 Vnode vnode

工作原理

Mithril 提供了一个 hyperscript 函数 m(),它可以用 JavaScript 语法表达任何 HTML 结构。它接受一个 selector 字符串(必须),一个 attributes 对象(可选)和一个 children 数组(可选)。

m("div", {id: "box"}, "hello")

// 等效 HTML:
// <div id="box">hello</div>

m() 函数返回的不是 DOM 元素,而是虚拟 DOM 节点或这 vnode,它是一个用 JavaScript 对象表示的 DOM 元素。

// a vnode
var vnode = {tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]}

可以使用 m.render() 函数把 vnode 转换为真实的 DOM 元素。

m.render(document.body, m("br")) // 在 <body> 中输出了 <br>

多次调用 m.render() 时,只会对 DOM 中发生变更的部分进行更改,并不会每次都重新创建 DOM 树,因为重新创建 DOM 是非常耗费资源的,且会导致失去输入框的焦点等问题。

灵活的参数

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() 函数的第一个参数可以是 CSS 选择器。它支持任何有效的 CSS 语法,包括:#(id),.(class)和 [](属性)等。

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='http://example.com']", "Leave")
// <a id="exit" class="external" href="http://example.com">Leave</a>

如果省略了标签名,Mithril 会默认使用 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() 函数的第一个参数和第二个参数中都存在 CSS 类名,则它们会合并到一起。

DOM 属性

Mithril 同时使用 JavaScript API 和 DOM API(setAttribute)来解析属性。这意味着你可以使用这两种语言来引用属性。

例如,在 JavaScript API 中,readonly 属性称为 element.readOnly(注意大小写)。在 Mithril 中,下面所有的用法都是支持的:

m("input", {readonly: true}) // 小写
m("input", {readOnly: true}) // 大写
m("input[readonly]")
m("input[readOnly]")

样式属性

Mithril 支持使用字符串或对象作为 style 的值。下面所有的用法都是支持的:

m("div", {style: "background:red;"})
m("div", {style: {background: "red"}})
m("div[style=background:red]")

使用字符串作为 style 的值时,在重绘时会覆盖元素中的所有内敛样式。

Mithril 不会为数字值添加单位。

事件

Mithril 支持为所有的事件绑定事件处理程序,包括不是以 on${event} 这种规范定义的属性,例如 touchstart

function doSomething(e) {
    console.log(e)
}

m("div", {onclick: doSomething})

属性

Mithril 支持可以通过属性访问的 DOM 功能,例如 <select>selectedIndexvalue 属性。

m("select", {selectedIndex: 0}, [
    m("option", "Option A"),
    m("option", "Option B"),
])

组件

组件可以把逻辑封装在一个单元内,并把它当元素使用。它是开发大型可扩展应用的基础。

组件是包含 view 方法的 JavaScript 对象。把组件作为 m() 函数的第一个参数传入,即可使用该组件。可以通过定义属性和子元素把参数传递给组件,如下面的示例所示:

// 定义一个组件
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 和组件拥有生命周期方法(又叫钩子),它们会在 DOM 元素的生命周期的不同时期被调用。Mithril 支持的生命周期方法包括:oninitoncreateonupdateonbeforeremoveonremoveonbeforeupdate

生命周期方法的定义和 DOM 事件处理函数的定义相同,但是传入了 vnode 作为参数:

function initialize(vnode) {
    console.log(vnode)
}

m("div", {oninit: initialize})
钩子 描述
oninit(vnode) vnode 被渲染成 DOM 元素之前运行
oncreate(vnode) vnode 添加到 DOM 以后运行
onupdate(vnode) 重绘完成后运行
onbeforeremove(vnode) DOM 元素被移除之前运行。如果返回 Promise,Mithril 会在 Promise 完成后再移除 DOM 元素。只有在移除元素本身时才会触发该方法,移除子元素时不会触发该方法。
onremove(vnode) DOM 元素被移除之前运行。如果定义了 onbeforeremove 钩子,则 onremove 方法会在调用 done 之后被调用。在移除元素本身、或移除元素的父元素时,都会触发该方法。
onbeforeupdate(vnode, old) onupdate 之前运行。如果它返回 false,则会阻止该元素及其子元素进行 diff。

要了解更多有关生命周期方法的信息,请参阅生命周期犯法页面。

key

列表中的 vnode 可以拥有一个称为 key 的特殊属性,用于对 DOM 元素进行标记。在生成 vnode 的模型数据改变时,可以通过 key 来找到指定的 DOM 元素。

通常,key 应该是数组中的唯一标志字段,即 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))

当列表中的 vnode 拥有 key 时,如果 users 数组被重新排序、视图被重新渲染,input 元素将按照和以前一致的排序方式进行排列,以便保持正确的焦点和 DOM 状态。

了解更多有关 key 的信息,参见 key 页面。

SVG 和 MathML

Mithril 完全支持 SVG。Xlink 也是支持的,但与之前 v1.0 版本的 Mithril 不同的是,必须明确定义命名空间:

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 表达式中,不能使用如 iffor 这样的 JavaScript 语句。最好全部使用上面的结构来写模版,以保持模版结构可读性和声明性。

HTML 转换为 hyperscript

在 Mithril 中,格式规范的 HTML 就是有效的 JSX 语法。要把一个 HTML 文件集成到一个使用 JSX 的项目中,除了复制粘贴外,要做的其他工作很少。

使用 hyperscript 时,需要把 HTML 代码转换为 hyperscript 语法,代码才能运行。为了方便,你可以使用 HTML-to-Mithril-template 模版转换器

避免反面模式

虽然 Mithril 很灵活,但有些代码模式还是不推荐使用:

避免使用动态选择器

不同的 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,
        ])
    }
}

避免在视图方法中使用 JavaScript 语句

JavaScript 语句通常需要更改 HTML 树的嵌套结构,使代码变得冗长和难以理解。创建虚拟 DOM 树的代码可能导致严重的性能问题(例如重新创建整个模版)。

// 避免这种用法
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 表达式,例如三元运算符和 Array 方法。

// 推荐这种用法
var BetterListComponent = {
    view: function() {
        return m("ul", vnode.attrs.items.map(function(item) {
            return m("li", item)
        }))
    }
}

避免在视图外创建 vnode

当重绘时遇到一个 vnode 和之前渲染的 vnode 严格相等时,会跳过这个 vnode,不会更新其内容。虽然这看起来是一个优化性能的方式,但应该避免这种用法,因为这阻止了那个节点的动态更新 - 这会导致副作用,例如重绘时无法触发生命周期方法。

组件的文档中包含更多细节和一个这种反面模式的例子