denounce函数:Javascript中如何应对高频触发事件?

在DOM Event的世界中,以scroll、resize、mouseover等为代表的高频触发事件显得有些与众不同。通常,DOM事件只有在明确的时间点才会被触发,比如被点击,比如XMLHttpRequest状态更改等等;而高频事件则是在整个动作时期内反复触发反复调用callback,为整个APP的流畅运行留下了性能隐患。

甚至w3school在介绍mousemove事件时还为大家留下了贴心小提示:

"每当用户把鼠标移动一个像素,就会发生一个 mousemove 事件。这会耗费系统资源去处理所有这些 mousemove 事件。因此请审慎地使用该事件。"

http://www.w3school.com.cn/jsref/event_onmousemove.asp

事实上,解决这类问题已经有了比较成熟的通用解决方案——denounce。denounce的核心思想在于,事件发生时,首先触发一个轻量级的proxy,再由这个轻量级的proxy去管理和调用真正的业务函数。这样的方案之所以能够提升效率,关键在于proxy并不会每次都去调用业务函数。"并不会每次都去调用"又是怎么实现的呢?其实就是前端开发者们再熟悉不过的setTimeout——由于高频函数的特点是反复快速触发,我们可以借助setTimeout去延迟调用业务函数;如果短时间内该事件被再次触发,由于setTimeout中的业务函数尚未被真正调用,我们尚有机会用clearTimeout取消其执行,并重新添加新的setTimeout调用;重复以上步骤,直至该动作(比如scroll或resize)结束不再触发事件,延迟顺利结束后,业务函数才被调用。

对denounce函数的实现不感兴趣的同学可以移步Underscore.js官网文档,了解一下denounce的使用方式;对实现感兴趣的同学可以继续阅读,下文将介绍本函数的具体实现,会比Underscore.js所提供的功能稍微全面一些哟。

首先来分析一下核心函数所需要的参数:作为事件监听机制的一种扩展,被监听的对象肯定是需要的;然后是所要监听的事件类型以及事件被触发时的callback;最后是setTimeout函数等待的时间delay。参数都有了,现在就来尝试实现核心函数:

/**
 * @param node 被监听的DOM对象
 * @param event 事件类型,比如scroll
 * @param callback 回调函数
 * @param delay 等待的毫秒数
 */
function denounce(node, event, callback, delay) {
    // 记录timeout id,在闭包中使用
    var timeout; 
function proxy(e) { // 如果短时间内再次被调用,则清除上次触发时设置的timeout clearTimeout(timeout); // 调用setTimeout函数添加延迟执行的逻辑 timeout = setTimeout(callback.bind(node, e), delay); } // 将proxy函数绑定到node的指定事件上 node.addEventListener(event, proxy, false); } // 绑定事件: denounce(window, 'scroll', function (e) { console.log(e); }, 200);

具体实现还是非常简单的,只要对setTimeout和clearTimeout有初步的理解,应该都可以轻松读懂上面这一段代码。当然,作为事件监听的一种应用,除了应该提供"on"方法之外,至少还应该提供接触事件绑定的"remove"方法。下文是相对比较全面的实现:

/**
 * Denounce对象,主要实现了on和remove方法,目前支持对scroll、resize和mousemove三种事件的延迟触发
 * 注意:本对象各方法未处理DOM事件监听的兼容性问题
 *
 * @object
 */
var Denounce = {
    // denounce id,用于记录事件和取消事件监听
    _id: 1,
    // 所支持的事件类型
    EVENTS: {
        SCROLL: 'scroll',
        RESIZE: 'resize',
        MOUSEMOVE: 'mousemove'
    },
    // 默认的setTimeout等待时间,可重置
    DELAY: 200,
    // 保存已有的监听事件
    listeners: {},

    /**
     * 用于监听事件的on方法
     *
     * @param node 被监听的DOM对象
     * @param event 事件类型,比如scroll
     * @param callback 回调函数
     * @param delay 等待的毫秒数
     * @return denounce id,用于取消事件监听
     */
    on: function (node, event, callback, delay) {
        var self = this;
        var id = self._id++;
delay = !!delay ? delay : self.DELAY; self.listeners[id] = { node: node, event: event, timeout: -1, proxy: function (e) { var listener = self.listeners[id]; clearTimeout(listener.timeout); listener.timeout = setTimeout(callback.bind(node, e), delay); } }; node.addEventListener(event, self.listeners[id].proxy, false); return id; }, /** * 用于取消事件监听的remove方法 * * @param denounce id 由on方法返回的id */ remove: function (id) { var self = this; var listener = self.listeners[id]; if (!listener) { return; } clearTimeout(listener.timeout); listener.node.removeEventListener(listener.event, listener.proxy, false); self.listeners[id] = null; } }; // 绑定事件 var id = Denounce.on(window, Denounce.EVENTS.SCROLL, function (e) { console.log(e); }, 200); // 取消绑定 Denounce.remove(id);

其中,delay设置的越大,动作结束后等待的延迟越久,反应较为“迟钝”,但被误判为动作结束的可能性较小;反之则延迟越低,反应较快,但被误判为动作结束的可能性较大。同学们可以根据自己的业务场景和需求自己调节该参数。(其中,delay设置的越大,资源消耗越低,但反应迟钝;delay设置的越小,资源消耗越高,但反应较快;同学们可以根据自己的业务场景和需求自己调节该参数。 该描述容易和throttle函数的用途混淆,感谢@木的树 同学批评指正)

(全文完)