天天看點

函數去抖(debounce)和函數節流(throttle)

目的

以下場景往往由于事件頻繁被觸發,因而頻繁執行DOM操作、資源加載等重行為,導緻UI停頓甚至浏覽器崩潰。

window對象的resize、scroll事件

拖拽時的mousemove事件

射擊遊戲中的mousedown、keydown事件

文字輸入、自動完成的keyup事件

實際上對于window的resize事件,實際需求大多為停止改變大小n毫秒後執行後續處理;而其他事件大多的需求是以一定的頻率執行後續處理。針對這兩種需求就出現了debounce和throttle兩種解決辦法。

throttle(又稱節流)和debounce(又稱去抖)其實都是函數調用頻率的控制器,

debounce去抖

當調用函數n秒後,才會執行該動作,若在這n秒内又調用該函數則将取消前一次并重新計算執行時間,舉個簡單的例子,我們要根據使用者輸入做suggest,每當使用者按下鍵盤的時候都可以取消前一次,并且隻關心最後一次輸入的時間就行了。

在lodash中提供了debounce函數:

_.debounce(func, [wait=0], [options={}])

1

lodash在opitons參數中定義了一些選項,主要是以下三個:

leading,函數在每個等待時延的開始被調用,預設值為false

trailing,函數在每個等待時延的結束被調用,預設值是true

maxwait,最大的等待時間,因為如果debounce的函數調用時間不滿足條件,可能永遠都無法觸發,是以增加了這個配置,保證大于一段時間後一定能執行一次函數

根據leading和trailing的組合,可以實作不同的調用效果:

leading-false,trailing-true:預設情況,即在延時結束後才會調用函數

leading-true,trailing-true:在延時開始時就調用,延時結束後也會調用

leading-true, trailing-false:隻在延時開始時調用

deboucne還有cancel方法,用于取消防抖動調用

下面是一些簡單的用例:

// 避免視窗在變動時出現昂貴的計算開銷。

jQuery(window).on('resize', _.debounce(calculateLayout, 150));

// 當點選時 `sendMail` 随後就被調用。

jQuery(element).on('click', _.debounce(sendMail, 300, {

  'leading': true,

  'trailing': false

}));

// 確定 `batchLog` 調用1次之後,1秒内會被觸發。

var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });

var source = new EventSource('/stream');

jQuery(source).on('message', debounced);

// 取消一個 trailing 的防抖動調用

jQuery(window).on('popstate', debounced.cancel);

在學習Vue的時候,官網也用到了一個裡子,就是用于對使用者輸入的事件進行了去抖,因為使用者輸入後需要進行ajax請求,如果不進行去抖會頻繁的發送ajax請求,是以通過debounce對ajax請求的頻率進行了限制

完整的demo在這裡。

methods: {
  // `_.debounce` 是一個通過 Lodash 限制操作頻率的函數。
  // 在這個例子中,我們希望限制通路 yesno.wtf/api 的頻率
  // AJAX 請求直到使用者輸入完畢才會發出。想要了解更多關于
  getAnswer: _.debounce(function() {
    if (!reg.test(this.question)) {
      this.answer = 'Questions usually end with a question mark. ;-)';
      return;
    }
    this.answer = 'Thinking ... ';
    let self = this;
    axios.get('https://yesno.wtf/api')
    // then中的函數如果不是箭頭函數,則需要對this指派self
    .then((response) = > {
      this.answer = _.capitalize(response.data.answer)
    }).
    catch ((error) = > {
      this.answer = 'Error! Could not reach the API. ' + error
    })
  }, 500) // 這是我們為判定使用者停止輸入等待的毫秒數
}
           

簡單的實作

一個簡單的手寫的去抖函數:

function test() {
  console.log(11)
}function debounce(method, context) {
  clearTimeout(method.tId);
  method.tId = setTimeout(function() {
    method.call(context)
  }, 500)
}
window.onresize = function() {
  debounce(test, window);
}
           

lodash中debounce的源碼學習

function debounce(func, wait, options) {
  var nativeMax = Math.max,
    toNumber,
    nativeMin

  var lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime,
    // func 上一次執行的時間
    lastInvokeTime = 0,
    leading = false,
    maxing = false,
    trailing = true;

  // func必須是函數
  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }

  // 對間隔時間的處理
  wait = toNumber(wait) || 0;

  // 對options中傳入參數的處理
  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;
  }

  // 在leading edge階段執行函數
  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time;
    // 為 trailing edge 觸發函數調用設定定時器
    timerId = setTimeout(timerExpired, wait);
    // leading = true 執行函數
    return leading ? invokeFunc(time) : result;
  }

  // 剩餘時間
  function remainingWait(time) {
    // 距離上次debounced函數被調用的時間
    var timeSinceLastCall = time - lastCallTime,
      // 距離上次函數被執行的時間
      timeSinceLastInvoke = time - lastInvokeTime,
      // 用 wait 減去 timeSinceLastCall 計算出下一次trailing的位置
      result = wait - timeSinceLastCall;
    // 兩種情況
    // 有maxing: 比較出下一次maxing和下一次trailing的最小值,作為下一次函數要執行的時間
    // 無maxing: 在下一次trailing時執行timerExpired
    return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
  }

  // 根據時間判斷 func 能否被執行
  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;
    // 幾種滿足條件的情況
    return (lastCallTime === undefined // 首次執行
      || (timeSinceLastCall >= wait) // 距離上次被調用已經超過 wait
      || (timeSinceLastCall < 0)// 系統時間倒退
      || (maxing && timeSinceLastInvoke >= maxWait)); //超過最大等待時間
  }

  // 在 trailing edge 且時間符合條件時,調用 trailingEdge函數,否則重新開機定時器
  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // 重新開機定時器
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  // 在trailing edge階段執行函數
  function trailingEdge(time) {
    timerId = undefined;
    // 有lastArgs才執行,
    // 意味着隻有 func 已經被 debounced 過一次以後才會在 trailing edge 執行
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    // 每次 trailingEdge 都會清除 lastArgs 和 lastThis,目的是避免最後一次函數被執行了兩次
    // 舉個例子:最後一次函數執行的時候,可能恰巧是前一次的 trailing edge,函數被調用,而這個函數又需要在自己時延的 trailing edge 觸發,導緻觸發多次
    lastArgs = lastThis = undefined;
    return result;
  }

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

  // flush方法--立即調用
  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }

  function debounced() {
    var time = now(),
      //是否滿足時間條件
      isInvoking = shouldInvoke(time);
    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time; //函數被調用的時間
    // 無timerId的情況有兩種:
    // 1.首次調用
    // 2.trailingEdge執行過函數
    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        // Handle invocations in a tight loop.
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    // 負責一種case:trailing 為 true 的情況下,在前一個 wait 的 trailingEdge 已經執行了函數;
    // 而這次函數被調用時 shouldInvoke 不滿足條件,是以要設定定時器,在本次的 trailingEdge 保證函數被執行
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }

  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}
           

throttle節流

throttle将一個函數的調用頻率限制在一定門檻值内,例如1s内一個函數不能被調用兩次。

同樣,lodash提供了這個方法:

_.throttle(func, [wait=0], [options={}])

具體使用的例子:

// 避免在滾動時過分的更新定位

jQuery(window).on('scroll', _.throttle(updatePosition, 100));

// 點選後就調用 `renewToken`,但5分鐘内超過1次。

var throttled = _.throttle(renewToken, 300000, { 'trailing': false });

jQuery(element).on('click', throttled);

// 取消一個 trailing 的節流調用。

jQuery(window).on('popstate', throttled.cancel);

throttle同樣提供了leading和trailing參數,與debounce含義相同

其實throttle就是設定了maxwait的debounce

注意,debounce傳回的是一個經過包裝的函數,被包裝的函數必須是要立刻執行的函數。例如:

function test() {
  console.log(123)
}
setInterval(function () {
  _.debounce(test, 1500)
}, 500)
           

上面的效果不會是我們想要的效果,因為每次setInterval執行之後,都傳回了一個沒有執行的、經過debounce包裝後的函數,是以debounce是無效的

點選事件也是同樣:

btn.addEventListener('click', function () {

  _.debounce(test, 1500)

})

上面的代碼同樣不會生效,正确的做法是:

btn.addEventListener('click', test)

setInterval(_.debounce(test, 1500), 500)

參考

http://www.css88.com/doc/lodash/#_debouncefunc-wait0-options

https://segmentfault.com/a/1190000012102372?utm_source=tuicool&utm_medium=referral

原文:https://blog.csdn.net/duola8789/article/details/78871789 

繼續閱讀