淺談節流與防抖

節流和防抖在開發項目過程當中很常見,例如 input 輸入實時搜索、scrollview 滾動更新了,等等,大量的場景須要咱們對其進行處理。咱們由 Lodash 來介紹,直接進入主題吧。前端

Lodash

API

  • 防抖 (debounce) :屢次觸發,只在最後一次觸發時,執行目標函數。git

    lodash.debounce(func, [wait=0], [options={}])
  • 節流(throttle):限制目標函數調用的頻率,好比:1s內不能調用2次。github

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

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

  • leading:函數在每一個等待時延的開始被調用,默認值爲false
  • trailing:函數在每一個等待時延的結束被調用,默認值是true
  • maxwait:最大的等待時間,由於若是 debounce 的函數調用時間不知足條件,可能永遠都沒法觸發,所以增長了這個配置,保證大於一段時間後必定能執行一次函數

根據 leading 和 trailing 的組合,能夠實現不一樣的調用效果:app

  • {leading: true, trailing: false}:只在延時開始時調用
  • {leading: false, trailing: true}:默認狀況,即在延時結束後纔會調用函數
  • {leading: true, trailing: true}:在延時開始時就調用,延時結束後也會調用

deboucne 還有 cancel 方法,用於取消防抖動調用函數

使用

  • 防抖 (debounce):this

    addEntity = () => {
      console.log('--------------addEntity---------------')
      this.debounceFun();
    }
     
    debounceFun = lodash.debounce(function(e){
      console.log('--------------debounceFun---------------');
    }, 500,{
      leading: true,
      trailing: false,
    })

    首次點擊時執行,連續點擊且時間間隔在500ms以內,再也不執行,間隔在500ms以外再次點擊,執行。code

  • 節流(throttle):對象

    addEntity = () => {
      console.log('--------------addEntity---------------');
      this.throttleFun();
    }
     
    throttleFun = lodash.throttle(function(e){
      console.log('--------------throttleFun---------------');
    }, 500,{
      leading: true,
      trailing: false,
    })

    首次點擊時執行,連續點擊且間隔在500ms以內,500ms以後自動執行一次(注:連續點擊次數時間以後小於500ms,則不會自動執行),間隔在500ms以外再次點擊,執行。blog

源碼實現

debounce

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,是否當即調用函數
 * @return {function}             返回客戶調用函數
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延遲執行函數
  const later = () => setTimeout(() => {
    // 延遲函數執行完畢,清空緩存的定時器序號
    timer = null
    // 延遲執行的狀況下,函數會在延遲函數中執行
    // 使用到以前緩存的參數和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這裏返回的函數是每次實際調用的函數
  return function(...params) {
    // 若是沒有建立延遲執行函數(later),就建立一個
    if (!timer) {
      timer = later()
      // 若是是當即執行,調用函數
      // 不然緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延遲執行函數(later),調用的時候清除原來的並從新設定一個
    // 這樣作延遲函數會從新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

throttle

/**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   若是想忽略開始函數的的調用,傳入{leading: false}。
 *                                若是想忽略結尾函數的調用,傳入{trailing: false}
 *                                二者不能共存,不然函數不能執行
 * @return {function}             返回客戶調用函數
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 以前的時間戳
    var previous = 0;
    // 若是 options 沒傳則設爲空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 若是設置了 leading,就將 previous 設爲 0
      // 用於下面函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 得到當前時間戳
      var now = _.now();
      // 首次進入前者確定爲 true
      // 若是須要第一次不執行函數
      // 就將上次時間戳設爲當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 若是當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
       // 若是設置了 trailing,只會進入這個條件
      // 若是沒有設置 leading,那麼第一次會進入這個條件
      // 還有一點,你可能會以爲開啓了定時器那麼應該不會進入這個 if 條件了
      // 其實仍是會進入的,由於定時器的延時
      // 並非準確的時間,極可能你設置了2秒
      // 可是他須要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 若是存在定時器就清理掉不然會調用二次回調
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
        // 沒有的話就開啓一個定時器
        // 而且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

走在最後,歡迎 star:https://github.com/sisterAn/blog

歡迎關注:前端瓶子君

相關文章
相關標籤/搜索