Fork me on Github

Javascript中 节流函数 throttle 与 防抖函数 debounce

问题的引出

  在一些场景往往由于事件频繁被触发,因而频繁地进行DOM操作、资源加载,导致UI停顿甚至浏览器崩溃。

在这样的情况下,我们实际上的需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce和throttle两种解决办法。

 

  1. resize事件

  2. mousemove事件

  3. touchmove事件

  4. scroll事件

  

throttle 与 debounce

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

原理:

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

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

区别:

  1. throttle 

    如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。

    也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。

  2.debounce

    如果用手指一直按住一个弹簧,它将不会弹起直到你松手为止。

    也就是说当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间。

 

简单代码实现及实验结果

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

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
jQuery throttle / debounce: Sometimes, less is more!
underscore.js

posted @ 2017-02-07 13:27  王者归来!  阅读(2736)  评论(0编辑  收藏  举报