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
      });
    }

參考

相關文章
相關標籤/搜索