在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调用;重复以上步骤,直至该事件不再触发,延迟顺利结束后,业务函数才被调用。
对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设置的越小,资源消耗越高,但反应较快;同学们根据自己的业务场景和需求自己调节该参数。
(全文完)