JS - debounce(去抖) 和 throttle(節流)

定義

爲了避免某個事件在較短的時間段內(稱爲 T)內連續觸發從而引起的其對應的事件處理函數不必要的連續執行的一種事件處理機制(高頻觸發事件解決方案)
debounce:當調用動作觸發一段時間後,纔會執行該動作,若在這段時間間隔內又調用此動作則將重新計算時間間隔。(多次連續事件觸發動作/最後一次觸發之後的指定時間間隔執行回調函數)
throttle:預先設定一個執行週期,當調用動作的時刻大於等於執行週期則執行該動作,然後進入下一個新的時間週期。(每個指定時間執行一次回調函數,可以指定時間間隔之前調用)

區別

1、throttle 保證了在每個 T 內至少執行一次,而 debounce 沒有這樣的保證
2、每次事件觸發時參考的時間點,對於debounce來是上一次事件觸發的時間並且在延時沒有結束時會重置延時;
throttle上一次 handler 執行的時間並且在延時尚未結束時不會重置延時

影響

響應速度跟不上觸發頻率,往往會出現延遲,導致假死或者卡頓感

實現

去抖 debounce

空閒控制:所有操作最後一次性執行

【簡潔版】

/**
* @param fn {Function}   實際要執行的函數
* @param delay {Number}  延遲時間,也就是閾值,單位是毫秒(ms)
* @return {Function}     返回一個“去彈跳”了的函數
*/
function debounce(fn, delay) {
  // 定時器,用來 setTimeout
  var timer

  // 返回一個函數,這個函數會在一個時間區間結束後的 delay 毫秒時執行 fn 函數
  return function () {
    // 保存函數調用時的上下文和參數,傳遞給 fn
    var context = this
    var args = arguments

    // 每次這個返回的函數被調用,就清除定時器,以保證不執行 fn
    clearTimeout(timer)

    // 當返回的函數被最後一次調用後(也就是用戶停止了某個連續的操作),
    // 再過 delay 毫秒就執行 fn
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

【完整版】

// immediate: 是否立即執行回調函數; 其它參數同上
function debounce(fn, wait, immediate) {
    let timer = null

    return function() {
        let args = [].slice.call(arguments)

        if (immediate && !timer) {
            fn.apply(this, args)
        }

        if (timer) clearTimeout(timer)
        timer = setTimeout(() => { //箭頭函數,this指向外層環境
            fn.apply(this, args)
        }, wait)
    }
}
// 測試:
var fn = function() {
    console.log('debounce..')
}
oDiv.addEventListener('click', debounce(fn, 3000))

節流 throttle

固定頻次:減少執行頻次,每隔一定時間執行一次

【簡潔版】

/**
* 固定回調函數執行的頻次
* @param fn {Function}   實際要執行的函數
* @param interval {Number}  執行間隔,單位是毫秒(ms)
*
* @return {Function}     返回一個“節流”函數
*/
var throttle = function (fn, interval) {
  // 記錄前一次時間
  var last = +new Date()
  var timer = null
  // 包裝完後返回 閉包函數
  return function () {
    var current = +new Date()
    var args = [].slice.call(arguments, 0)
    var context = this
    // 首先清除定時器
    clearTimeout(timer)
    // current 與last 間隔大於interval 執行一次fn
    // 在一個週期內 last相對固定 current一直再增加
    // 這裏可以保證調用很密集的情況下 current和last 必須是相隔interval 纔會調用fn
    if (current - last >= interval) {
      fn.apply(context, args)
      last = current
    } else {
      // 如果沒有大於間隔 添加定時器
      // 這可以保證 即使後面沒有再次觸發 fn也會在規定的interval後被調用
      timer = setTimeout(function() {
        fn.apply(context, args)
        last = current
      }, interval-(current - last))
    }
  }
}

【完整版】

/**
 * 頻率控制 返回函數連續調用時,func 執行頻率限定爲 次 / wait
 * 自動合併 data
 * 
 * 若無 option 選項,或者同時爲true,即 option.trailing !== false && option.leading !== false,在固定時間開始時刻調用一次回調,並每個固定時間最後時刻調用回調
 * 若 option.trailing !== false && option.leading === false, 每個固定時間最後時刻調用回調
 * 若 option.trailing === false && option.leading  !== false,  只會在固定時間開始時刻調用一次回調
 * 若同時爲false 則不會被調用
 *
 * @param  {function}   func      傳入函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始邊界上的調用,傳入{leading: false}。默認undefined
 *                                如果想忽略結尾邊界上的調用,傳入{trailing: false}, 默認undefined
 * @return {function}             返回客戶調用函數
 */
function throttle (func, wait, options) {
  var context, args, result;
  var timeout = null;
  // 上次執行時間點
  var previous = 0;
  if (!options) { options = {}; }
  // 延遲執行函數
  function later () {
    // 若設定了開始邊界不執行選項,上次執行時間始終爲0
    previous = options.leading === false ? 0 : Date.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) { context = args = null; }
  }
  return function (handle, data) {
    var now = Date.now();
    // 首次執行時,如果設定了開始邊界不執行選項,將上次執行時間設定爲當前時間。
    if (!previous && options.leading === false) { previous = now; }
    // 延遲執行時間間隔
    var remaining = wait - (now - previous);
    context = this;
    args = args ? [handle, Object.assign(args[1], data)] : [handle, data];
    // 延遲時間間隔remaining小於等於0,表示上次執行至此所間隔時間已經超過一個時間窗口
    // remaining大於時間窗口wait,表示客戶端系統時間被調整過
    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
  }
}

運用

  • 遊戲射擊,keydown 事件
  • 文本輸入、自動完成,keyup 事件
  • 鼠標移動,mousemove 事件
  • DOM 元素動態定位,window 對象的 resize 和 scroll 事件

前兩者 debounce 和 throttle 都可以按需使用;後兩者肯定是用 throttle

underscore 實現源碼

debounce

_.debounce = function(func, wait, immediate) {
  var timeout, result;

  var later = function(context, args) {
    timeout = null;
    if (args) result = func.apply(context, args);
  };

  var debounced = restArgs(function(args) {
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      var callNow = !timeout;
      timeout = setTimeout(later, wait);
      if (callNow) result = func.apply(this, args);
    } else {
      timeout = _.delay(later, wait, this, args);
    }

    return result;
  });

  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
};

throttle

 _.throttle = function(func, wait, options) {
  var timeout, context, args, result;
  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;
  };

  var throttled = 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) {
      if (timeout) {
        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;
  };

  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
};

【參考】
https://blog.coding.net/blog/...
https://github.com/lishengzxc...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章