在DOM Event的世界中,以scroll、resize、mouseover等为表明的高频触发事件显得有些不同凡响。一般,DOM事件只有在明确的时间点才会被触发,好比被点击,好比XMLHttpRequest状态更改等等;而高频事件则是在整个动做时期内反复触发反复调用callback,为整个APP的流畅运行留下了性能隐患。前端
甚至w3school在介绍mousemove事件时还为你们留下了贴心小提示:
"每当用户把鼠标移动一个像素,就会发生一个 mousemove 事件。这会耗费系统资源去处理全部这些 mousemove 事件。所以请审慎地使用该事件。"
http://www.w3school.com.cn/jsref/event_onmousemove.aspnode
事实上,解决这类问题已经有了比较成熟的通用解决方案——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"方法。下文是相对比较全面的实现:this
/** * 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函数的用途混淆,感谢@木的树 同窗批评指正)spa
(全文完)code