转载

为性能而生的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调用;重复以上步骤,直至该事件不再触发,延迟顺利结束后,业务函数才被调用。

对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设置的越小,资源消耗越高,但反应较快;同学们根据自己的业务场景和需求自己调节该参数。

(全文完)

正文到此结束
Loading...