throttle函数与debounce函数

throttle函数与debounce函数

有时候,咱们会对一些触发频率较高的事件进行监听,若是在回调里执行高性能消耗的操做,反复触发时会使得性能消耗提升,浏览器卡顿,用户使用体验差。或者咱们须要对触发的事件延迟执行回调,此时能够借助throttle/debounce函数来实现需求。javascript

throttle函数

throttle函数用于限制函数触发的频率,每一个delay时间间隔,最多只能执行函数一次。一个最多见的例子是在监听resize/scroll事件时,为了性能考虑,须要限制回调执行的频率,此时便会使用throttle函数进行限制。css

由throttle函数的定义可知,每一个delay时间间隔,最多只能执行函数一次,因此须要有一个变量来记录上一个执行函数的时刻,再结合延迟时间和当前触发函数的时刻来判断当前是否能够执行函数。在设定的时间间隔内,函数最多只能被执行一次。同时,第一次触发时当即执行函数。如下为throttle实现的简略代码:java

function throttle(fn, delay) {
    var timer;
    return function() {
        var last = timer;
        var now = Date.now();
        if(!last) {
          timer = now;
          fn.apply(this,arguments);
          return;
        }
        if(last + delay > now) return;
        timer = now;
        fn.apply(this,arguments);
    }
}

debounce函数

debounce函数一样能够减小函数触发的频率,但限制的方式有点不一样。当函数触发时,使用一个定时器延迟执行操做。当函数被再次触发时,清除已设置的定时器,从新设置定时器。若是上一次的延迟操做还未执行,则会被清除。一个最多见的业务场景是监听onchange事件,根据用户输入进行搜索,获取远程数据。为避免屡次ajax请求,使用debounce函数做为onchange的回调。jquery

由debounce的用途可知,实现延迟回调须要用到setTimeout设置定时器,每次从新触发时须要清除原来的定时器并从新设置,简单的代码实现以下:git

function debounce(fn, delay){
    var timer;
    return function(){
        if(timer) clearTimeout(timer)
        timer = setTimeout(()=>{
            timer = undefined
            fn.apply(this, arguments);
        }, delay||0)
    }
}

小结

throttle函数与debounce函数的区别就是throttle函数在触发后会立刻执行,而debounce函数会在必定延迟后才执行。从触发开始到延迟结束,只执行函数一次。上文中throttle函数实现并未使用定时器,开源类库提供的throttle方法大多使用定时器实现,并且开源经过参数配置项,区分throttle函数与debounce函数。github

实现throttle和debounce的开源库

上文中实现的代码较为简单,未考虑参数类型的判断及配置、测试等。下面介绍部分实现throttle和debounce的开源的类库。ajax

jQuery.throttle jQuery.debounce

$.throttle指向函数jq_throttlejq_throttle接收四个参数 delay, no_trailing, callback, debounce_mode。参数二no_trailing在throttle模式中指示。除了在文档上说明的三个参数外,第四个参数debounce_mode用于指明是不是debounce模式,真即debounce模式,不然是throttle模式。segmentfault

jq_throttle函数内,先声明须要使用的变量timeout_id(定时器)和last_exec(上一次执行操做的时间),进行了参数判断和交换,而后定义了内部函数wrapper,做为返回的函数。浏览器

wrapper内,有用于更新上次执行操做的时刻并执行真正的操做的函数exec,用于清除debounce模式中定时器的函数clear,保存当前触发时刻和上一次执行操做时刻的时间间隔的变量elapsedapp

若是是debounce模式且timeout_id空,执行exec。若是定时器timeout_id存在则清除定时器。

若是是throttle模式且elapsed大于延迟时间delay,执行exec;不然,当no_trainling非真时,更新timeout_id,从新设置定时器,补充在上面清除的定时器:若是是debounce模式,执行timeout_id = setTimeout(clear, delay),若是是throttle模式,执行timeout_id = setTimeout(exec, delay - elapsed)

$.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) {
    // After wrapper has stopped being called, this timeout ensures that
    // `callback` is executed at the proper times in `throttle` and `end`
    // debounce modes.
    var timeout_id,
      
      // Keep track of the last time `callback` was executed.
      last_exec = 0;
    
    // `no_trailing` defaults to falsy.
    if ( typeof no_trailing !== 'boolean' ) {
      debounce_mode = callback;
      callback = no_trailing;
      no_trailing = undefined;
    }
    
    // The `wrapper` function encapsulates all of the throttling / debouncing
    // functionality and when executed will limit the rate at which `callback`
    // is executed.
    function wrapper() {
      var that = this,
        elapsed = +new Date() - last_exec,
        args = arguments;
      
      // Execute `callback` and update the `last_exec` timestamp.
      function exec() {
        last_exec = +new Date();
        callback.apply( that, args );
      };
      
      // If `debounce_mode` is true (at_begin) this is used to clear the flag
      // to allow future `callback` executions.
      function clear() {
        timeout_id = undefined;
      };
      
      if ( debounce_mode && !timeout_id ) {
        // Since `wrapper` is being called for the first time and
        // `debounce_mode` is true (at_begin), execute `callback`.
        exec();
      }
      
      // Clear any existing timeout.
      timeout_id && clearTimeout( timeout_id );
      
      if ( debounce_mode === undefined && elapsed > delay ) {
        // In throttle mode, if `delay` time has been exceeded, execute
        // `callback`.
        exec();
        
      } else if ( no_trailing !== true ) {
        // In trailing throttle mode, since `delay` time has not been
        // exceeded, schedule `callback` to execute `delay` ms after most
        // recent execution.
        // 
        // If `debounce_mode` is true (at_begin), schedule `clear` to execute
        // after `delay` ms.
        // 
        // If `debounce_mode` is false (at end), schedule `callback` to
        // execute after `delay` ms.
        timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay );
      }
    };
    
    // Set the guid of `wrapper` function to the same of original callback, so
    // it can be removed in jQuery 1.4+ .unbind or .die by using the original
    // callback as a reference.
    if ( $.guid ) {
      wrapper.guid = callback.guid = callback.guid || $.guid++;
    }
    
    // Return the wrapper function.
    return wrapper;
  };

debounce函数内部实际调用了throttle函数。

$.debounce = function( delay, at_begin, callback ) {
    return callback === undefined
      ? jq_throttle( delay, at_begin, false )
      : jq_throttle( delay, callback, at_begin !== false );
  };

lodash的throttle与debounce

lodash中相比jQuery,提供了leadingtrailing选项,表示在函数在等待开始时被执行和函数在等待结束时被执行。而对于debounce函数,还提供了maxWait,当debounce函数重复触发时,有可能因为wait过长,回调函数没机会执行,maxWait字段确保了当函数重复触发时,每maxWait毫秒执行函数一次。

maxWait的做用,咱们能够联想到,提供maxWait的debounce函数与throttle函数的做用是同样的;事实上,lodash的throttle函数就是指明maxWait的debounce函数。

lodash从新设置计时器时,并无调用clearTimeout清除定时器,而是在执行回调前判断参数和执行上下文是否存在,存在时则执行回调,执行完以后将参数和上下文赋值为undefined;重复触发时,参数和上下文为空,不执行函数。这也是与jQuery实现的不一样之一

如下为debounce函数内的函数和变量:

  • 局部变量lastInvokeTime记录上次执行时间,默认0
  • 函数invokeFunc执行回调操做,并更新上一次执行时间lastInvokeTime
  • 函数leadingEdge设置定时器,并根据传参配置决定是否在等待开始时执行函数。
  • 函数shouldInvoke判断是否能够执行回调函数。
  • 函数timerExpired判断是否能够当即执行函数,若是能够则执行,不然从新设置定时器,函数remainingWait根据上次触发时间/执行时间和当前时间返回从新设置的定时器的时间间隔。
  • 函数trailingEdge根据配置决定是否执行函数,并清空timerId
  • 函数cancel取消定时器,并重置内部参数。函数debounced是返回的内部函数。

debounced内部先获取当前时间time,判断是否能执行函数。若是能够执行,且timerId空,表示能够立刻执行函数(说明是第一次触发或已经执行过trailingEdge),执行leadingEdge,设置定时器。

若是timerId非空且传参选项有maxWait,说明是throttle函数,设置定时器延迟执行timerExpired并当即执行invokeFunc,此时在timerExpired中设置的定时器的延迟执行时间是wait - timeSinceLastCallmaxWait - timeSinceLastInvoke的最小值,分别表示经过wait设置的仍需等待执行函数的时间(下一次trailing的时间)和经过maxWait设置的仍需等待执行函数的时间(下一次maxing的时间)。

function debounce(func, wait, options) {
      var lastArgs,
          lastThis,
          maxWait,
          result,
          timerId,
          lastCallTime,
          lastInvokeTime = 0,
          leading = false,
          maxing = false,
          trailing = true;

      if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      wait = toNumber(wait) || 0;
      if (isObject(options)) {
        leading = !!options.leading;
        maxing = 'maxWait' in options;
        maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
        trailing = 'trailing' in options ? !!options.trailing : trailing;
      }

      function invokeFunc(time) {
        var args = lastArgs,
            thisArg = lastThis;

        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        result = func.apply(thisArg, args);
        return result;
      }

      function leadingEdge(time) {
        // Reset any `maxWait` timer.
        lastInvokeTime = time;
        // Start the timer for the trailing edge.
        timerId = setTimeout(timerExpired, wait);
        // Invoke the leading edge.
        return leading ? invokeFunc(time) : result;
      }

      function remainingWait(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime,
            timeWaiting = wait - timeSinceLastCall;

        return maxing
          ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
          : timeWaiting;
      }

      function shouldInvoke(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime;

        // Either this is the first call, activity has stopped and we're at the
        // trailing edge, the system time has gone backwards and we're treating
        // it as the trailing edge, or we've hit the `maxWait` limit.
        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
      }

      function timerExpired() {
        var time = now();
        if (shouldInvoke(time)) {
          return trailingEdge(time);
        }
        // Restart the timer.
        timerId = setTimeout(timerExpired, remainingWait(time));
      }

      function trailingEdge(time) {
        timerId = undefined;

        // Only invoke if we have `lastArgs` which means `func` has been
        // debounced at least once.
        if (trailing && lastArgs) {
          return invokeFunc(time);
        }
        lastArgs = lastThis = undefined;
        return result;
      }

      function cancel() {
        if (timerId !== undefined) {
          clearTimeout(timerId);
        }
        lastInvokeTime = 0;
        lastArgs = lastCallTime = lastThis = timerId = undefined;
      }

      function flush() {
        return timerId === undefined ? result : trailingEdge(now());
      }

      function debounced() {
        var time = now(),
            isInvoking = shouldInvoke(time);

        lastArgs = arguments;
        lastThis = this;
        lastCallTime = time;

        if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }
        return result;
      }
      debounced.cancel = cancel;
      debounced.flush = flush;
      return debounced;
    }

throttle函数则是设置了maxWait选项且leading为真的debounce函数。

function throttle(func, wait, options) {
      var leading = true,
          trailing = true;

      if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      if (isObject(options)) {
        leading = 'leading' in options ? !!options.leading : leading;
        trailing = 'trailing' in options ? !!options.trailing : trailing;
      }
      return debounce(func, wait, {
        'leading': leading,
        'maxWait': wait,
        'trailing': trailing
      });
    }

参考

相关文章
相关标签/搜索