试议常用Javascript 类库中 throttle 与 debounce 辅助函数的区别

问题的引出

看过我前面两篇博客的童鞋可能会注意到都谈到了事件处理的优化问题。

在很多应用中,我们需要控制函数执行的频率,

例如 窗口的 resize,窗口的 scroll 等操作,事件触发的频率非常高,如果处理函数比较复杂,需要较多的计算时间,那么会加重浏览器的负担,这时我们很自然会想到:能否在不影响显示效果(对显示效果影响在可接受范围内)的前提下减少事件响应函数的执行频率呢?

朴素的解决思路

  1. 首先我们会想到设置一定的时间范围delay,每隔delay ms 执行不超过一次。
    事件处理函数什么时候执行能? 这里有两个选择,一是先执行,再间隔delay ms来等待;或者是先等待delay ms,然后执行事件处理函数。

  2. 操作过程中的事件全不管,反正只执行一次事件处理。
    相同低,这一次的事件处理可以是先执行一次,然后后面的事件都不管; 或者前面的都不管,最后操作完了再执行一次事件处理。

当然上面是简单的通俗描述。

throttle 与 debounce 函数现状

throttle与debounce即是对上面两种思路的具体实现。

在现在很多的javascript框架中都提供了这两个函数。例如 jquery中有throttle和debounce插件, underscore.js ,Lo-dash.js 等都提供了这两个函数。

释义

throttle
在英文中是节流阀的意思,顾名思义,就像原本一直流水的龙头,现在每隔一定时间开一下,以节流。

*下图 “|” 表示throttle函数的一次执行, x 表示事件处理函数fn的一次执行*

|||||||||||||||||||||||| (暂停) ||||||||||||||||||||||||

X           X           X        X           X           X

debounce
bounce是名词弹力,或者动词反弹的意思, de-表否定/相反的前缀,形象低表示一直持续低压抑住不让反弹,当然最后还是得松手,那么反弹一次。

|||||||||||||||||||||||| (暂停) ||||||||||||||||||||||||  

X                       X        X                       X

*上图 “|” 表示debounce函数的一次执行, x 表示事件处理函数fn的一次执行*

区别

throttle 可以想象成阀门一样定时打开来调节流量。 debounce可以想象成把很多事件压缩组合成了一个事件。

有个生动的类比,假设你在乘电梯,没次进一个人需等待10秒钟,不考虑电梯容量限制,那么两种不同的策略是:

谢谢 wonyun ,已改正:

debounce 你在进入电梯后发现这时不远处走来了了一个人,等10秒钟,这个人进电梯后不远处又有个妹纸姗姗来迟,怎么办,再等10秒,于是妹纸上电梯时又来了一对好基友...,作为感动中国好码农,你要每进一个人就等10秒,直到没有人进来,10秒超时,电梯开动。

throttle 电梯很忙,每次就只等10秒,不管是来了妹纸还是好基友,电梯每隔10秒准时送一波人。

因此,简单来说,debounce适合只执行一次的情况,例如 搜索框中的自动完成。在停止输入后提交一次ajax请求;
而throttle适合指定每隔一定时间间隔内执行不超过一次的情况,例如拖动滚动条,移动鼠标的事件处理等。

简单代码实现及实验结果

那么下面我们自己简单地实现下这两个函数:

throttle 函数:

  window.addEventListener("resize", throttle(callback, 300, {leading:false}));
  window.addEventListener("resize", callback2);
  function callback ()  { console.count("Throttled"); }
  function callback2 () { console.count("Not Throttled"); }
 /**
  * 频率控制函数, fn执行次数不超过 1 次/delay
  * @param fn{Function}     传入的函数
  * @param delay{Number}    时间间隔
  * @param options{Object}  如果想忽略开始边界上的调用则传入 {leading:false},
  *                         如果想忽略结束边界上的调用则传入 {trailing:false},
  * @returns {Function}     返回调用函数
  */
 function throttle(fn,delay,options) {
     var wait=false;
     if (!options) options = {};
     return function(){
         var that = this,args=arguments;
         if(!wait){
             if (!(options.leading === false)){
                 fn.apply(that,args);
             }
             wait=true;
             setTimeout(function () {
                 if (!(options.trailing === false)){
                     fn.apply(that,args);
                 }
                 wait=false;
             },delay);
         }
     }
 }

将以上代码贴入浏览器中运行,可得到:

下面再看debounce函数的情况,
debounce 函数:

window.addEventListener("resize", throttle(callback, 300, {leading:false}));
window.addEventListener("resize", callback2);
function callback ()  { console.count("Throttled"); }
function callback2 () { console.count("Not Throttled"); }
/**
 * 空闲控制函数, fn仅执行一次
 * @param fn{Function}     传入的函数
 * @param delay{Number}    时间间隔
 * @param options{Object}  如果想忽略开始边界上的调用则传入 {leading:false},
 *                         如果想忽略结束边界上的调用则传入 {trailing:false},
 * @returns {Function}     返回调用函数
 */
function debounce(fn, delay, options) {
    var timeoutId;
    if (!options) options = {};
    var leadingExc = false;

    return function() {
        var that = this,
            args = arguments;
        if (!leadingExc&&!(options.leading === false)) {
            fn.apply(that, args);
        }
        leadingExc=true;
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        timeoutId = setTimeout(function() {
            if (!(options.trailing === false)) {
                fn.apply(that, args);
            }
            leadingExc=false;
        }, delay);
    }
}

将以上代码贴入浏览器中运行,分三次改变窗口大小,可看到,每一次改变窗口的大小都会把开始和结束边界的事件处理函数各执行一次:

如果是一次性改变窗口大小,会发现开始和结束的边界各执行一次时间处理函数,请注意与一次性改变窗口大小时 throttle 情况的对比:

underscore.js 的代码实现

_.throttle函数

 /**
  * 频率控制函数, fn执行次数不超过 1 次/delay
  * @param fn{Function}     传入的函数
  * @param delay{Number}    时间间隔
  * @param options{Object}  如果想忽略开始边界上的调用则传入 {leading:false},
  *                         如果想忽略结束边界上的调用则传入 {trailing:false},
  * @returns {Function}     返回调用函数
  */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    var previous = 0;
    if (!options) options = {};
    var later = function() {
        previous = options.leading === false ? 0 : _.now();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };
    return function() {
        var now = _.now();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            clearTimeout(timeout);
            timeout = null;
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };
};

_.debounce函数

/**
 * 空闲控制函数, fn仅执行一次
 * @param fn{Function}     传入的函数
 * @param delay{Number}    时间间隔
 * @param options{Object}  如果想忽略开始边界上的调用则传入 {leading:false},
 *                         如果想忽略结束边界上的调用则传入 {trailing:false},
 * @returns {Function}     返回调用函数
 */
_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
        var last = _.now() - timestamp;
        if (last < wait && last > 0) {
            timeout = setTimeout(later, wait - last);
        } else {
            timeout = null;
            if (!immediate) {
                result = func.apply(context, args);
                if (!timeout) context = args = null;
            }
        }
    };

    return function() {
        context = this;
        args = arguments;
        timestamp = _.now();
        var callNow = immediate && !timeout;
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
            context = args = null;
        }
        return result;
    };
};   

###参考链接###: [Debounce and Throttle: a visual explanation](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) [jQuery throttle / debounce: Sometimes, less is more!](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) [underscore.js](http://underscorejs.org/)
posted @ 2014-11-17 09:01  Tong Zeng  阅读(4861)  评论(2编辑  收藏  举报