Mithril 生命周期方法

生命周期方法

用法

组件虚拟 DOM 节点都有生命周期方法,也叫钩子,它们会在 DOM 元素的生命周期的对应时期被调用。

// 组件中的钩子
var ComponentWithHook = {
    oninit: function(vnode) {
        console.log("initialize component")
    },
    view: function() {
        return "hello"
    }
}

// vnode 中的钩子
function initializeVnode() {
    console.log("initialize vnode")
}

m(ComponentWithHook, {oninit: initializeVnode})

所有生命周期方法都使用 vnode 作为第一个参数,并把 this 关键字绑定到了 vnode.state

生命周期方法只有通过 Mithril 的 m.render() 调用时才会触发。如果不通过 Mithril,而是直接修改 DOM,则不会触发生命周期方法。

DOM 元素的生命周期

DOM 元素被创建后通常会追加到 document 中。当触发事件或更新数据时,可能会导致 DOM 元素的属性和子节点更新;也有可能导致元素从 document 中移除。

移除元素后,可能会临时保存在内存池中。内存池中的元素可能会在随后的更新中(DOM 回收的过程)重新使用。回收元素可以避免重新创建元素导致的性能开销。

oninit

oninit(vnode) 钩子在 vnode 初始化前被调用。oninit 会在 DOM 元素添加到 document 之前运行,并且会先运行父元素上的 oninit,再运行子元素上的。但使用该方法时不能确保 DOM 元素已经存在,所以你不应该从 oninit 方法中访问 vnode.dom

当更新一个元素时,不会调用该钩子。但如果元素已经被回收,然后再更新时,就会调用该钩子。

该钩子可用于通过 vnode.attrsvnode.children 传入的参数来初始化组件状态。

var ComponentWithState = {
    oninit: function(vnode) {
        this.data = vnode.attrs.data
    },
    view: function() {
        return m("div", this.data) // displays data from initialization time
    }
}

m(ComponentWithState, {data: "Hello"})

// 等效 HTML
// <div>Hello</div>

不要在该方法中修改模型数据。因为 oninit 被调用时其他元素的状态无法确认,在下一次渲染循环前,该方法中的模型更改可能不会在 UI 中体现出来。

oncreate

在 DOM 元素创建完成并添加到 document 后,会调用 oncreate(vnode) 钩子。oncreate 会在渲染循环结束后运行,所以在该方法中,可以读取 vnode.dom.offsetHeightvnode.dom.getBoundingClientRect() 等布局的值。

当更新元素时,不会调用该钩子。

和其他钩子一样,oncreate 回调中的 this 关键字指向 vnode.state。vnode 中具有 oncreate 钩子的 DOM 元素不会被回收。

oncreate 钩子可用于读取布局的值,这些布局可能会触发重绘、执行动画、或者初始化需要引用 DOM 元素的第三方库。

var HeightReporter = {
    oncreate: function(vnode) {
        console.log("Initialized with height of: ", vnode.dom.offsetHeight)
    },
    view: function() {}
}

m(HeightReporter, {data: "Hello"})

不要在该方法中修改模型数据。因为 oncreate 会在渲染结束时执行,所以在下一次渲染循环前,在该方法中对模型的更改不会体现在 UI 中。

onupdate

onupdate 钩子会在 DOM 元素被更新后调用。onupdate 会在渲染循环结束后运行,所以可以放心的在该方法中读取布局值,例如 vnode.dom.offsetHeightvnode.dom.getBoundingClientRect()

只有当前的渲染循环中存在该元素时,才会调用该钩子。元素被创建或回收时,不会调用该钩子。

像其他钩子一样,onupdate 回调中的 this 关键字指向 vnode.state。vnode 中含有 onupdate 钩子的 DOM 元素不会被回收。

onupdate 钩子可用于读取会触发重绘的布局的值,以及在模型数据更改后,动态更新第三方库中的 UI 元素的状态。

var RedrawReporter = {
    count: 0,
    onupdate: function(vnode) {
        console.log("Redraws so far: ", ++vnode.state.count)
    },
    view: function() {}
}

m(RedrawReporter, {data: "Hello"})

onbeforeremove

onbeforeremove(vnode) 钩子在 DOM 元素从 document 中分离之前被调用。如果返回 Promise,则会在 Promise 完成后再分离。

该钩子只有在元素失去父元素时才会被调用,当失去子元素时,不会调用该钩子。

像其他钩子一样,onbeforeremove 回调中的 this 关键字指向 vnode.state。vnode 中含 onbeforeremove 钩子的 DOM 元素不会被回收。

var Fader = {
    onbeforeremove: function(vnode) {
        vnode.dom.classList.add("fade-out")
        return new Promise(function(resolve) {
            setTimeout(resolve, 1000)
        })
    },
    view: function() {
        return m("div", "Bye")
    },
}

onremove

onremove(vnode) 钩子在 DOM 元素被移除之前调用。如果同时定义了 onbeforeremove 钩子,onremove 会在从 onbeforeremove 返回的 Promise 完成之后调用。

该钩子会在任何从 document 中移除的元素上被调用,无论是它直接被移除,还是它的父元素被移除。

和其他钩子一样,onremove 回调中的 this 关键字指向 vnode.state。vnode 中含 onremove 钩子的 DOM 元素不会被回收。

onremove 钩子可以用于进行清理工作。

var Timer = {
    oninit: function(vnode) {
        this.timeout = setTimeout(function() {
            console.log("timed out")
        }, 1000)
    },
    onremove: function(vnode) {
        clearTimeout(this.timeout)
    },
    view: function() {}
}

onbeforeupdate

onbeforeupdate(vnode, old) 钩子会在对 vnode 进行 diff 之前调用。如果定义了该函数,且返回 false,Mithril 会阻止对 vnode 进行 diff,同时也阻止了对 vnode 的子元素进行 diff。

这个钩子本身不会阻止生成虚拟 DOM 子树,除非子树被封装在一个组件中。

像其他钩子一样,onbeforeupdate 回调中的 this 关键字指向 vnode.state

该钩子可用于在 DOM 树过大时,减少更新的延迟。

避免反面模式

避免过早优化

onbeforeupdate 来跳过 diff 只能作为性能优化的最后的手段,除非你遇到了明显的性能问题,否则应该避免使用它。

通常可以通过 onbeforeupdate 来修复的性能问题,都是因为数组过大,这里的 “大” 是指一个数组包含了大量的节点,无论是数组的宽度(例如超过 5000 行的表格),还是数组嵌套的深度。

如果你确实遇到性能问题,首先考虑你的 UI 设计是否合理,否则,请修改你的 UI 设计。例如,用户不会希望在一个 5000 行的表格中来选取数据,这时只返回最相关的几条数据会有更好的用户体验。

如果你必须用到大量的 DOM 元素,可以在大数组的父元素上使用 onbeforeupdate 来重新评估性能。大多数情况下,单次检查应该是足够的;在罕见的情况下,需要重复多次检查,但是在使用多个 onbeforeupdate 时应该越来越谨慎,因为多个 onbeforeupdate 是一种代码异味,指示设计流程中的优先级问题。

不要出于 “以防万一” 的目的提前进行性能优化。记住,一般来说,更多的代码意味着更高的维护成本。当你使用 onbeforeupdate 来对元素进行有条件的 diff 时,同时也会导致相关的错误难以排查。

再次强调,onbeforeupdate 只能作为性能优化的最后的手段。