防抖和節(jié)流?區(qū)別以及實現(xiàn)

防抖和節(jié)流的作用都是防止函數(shù)多次調用。區(qū)別在于,假設一個用戶一直觸發(fā)這個函數(shù),且每次觸發(fā)函數(shù)的間隔小于wait,防抖的情況下只會調用一次,而節(jié)流的情況會每隔一定時間(參數(shù)wait)調用函數(shù)。

1.防抖
觸發(fā)高頻事件后n秒內函數(shù)只會執(zhí)行一次,如果n秒內高頻事件再次被觸發(fā),則重新計算時間

思路:
每次觸發(fā)事件時都取消之前的延時調用方法

先來一個簡單版的防抖

// func是用戶傳入需要防抖的函數(shù)
// wait是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器id
  let timer = 0
  // 這里返回的函數(shù)是每次用戶實際調用的防抖函數(shù)
  // 如果已經設定過定時器了就清空上一次的定時器
  // 開始一個新的定時器,延遲執(zhí)行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不難看出如果用戶調用該函數(shù)的間隔小于wait的情況下,上一次的時間還未到就被清除了,并不會執(zhí)行函數(shù)

這是一個簡單版的防抖,這個防抖只能在最后調用,一般的防抖會有immediate選項,表示是否立即調用

下面我們來實現(xiàn)一個帶有立即執(zhí)行選項的防抖函數(shù)

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

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

  // 這里返回的函數(shù)是每次實際調用的函數(shù)
  return function(...params) {
    // 如果沒有創(chuàng)建延遲執(zhí)行函數(shù)(later),就創(chuàng)建一個
    if (!timer) {
      timer = later()
      // 如果是立即執(zhí)行,調用函數(shù)
      // 否則緩存參數(shù)和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延遲執(zhí)行函數(shù)(later),調用的時候清除原來的并重新設定一個
    // 這樣做延遲函數(shù)會重新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

總結:

  • 對于按鈕防點擊來說的實現(xiàn):如果函數(shù)是立即執(zhí)行的,就立即調用,如果函數(shù)是延遲執(zhí)行的,就緩存上下文和參數(shù),放到延遲函數(shù)中去執(zhí)行。一旦我開始一個定時器,只要我定時器還在,你每次點擊我都重新計時。一旦你點累了,定時器時間到,定時器重置為 null,就可以再次點擊了。
  • 對于延時執(zhí)行函數(shù)來說的實現(xiàn):清除定時器ID,如果是延遲調用就調用函數(shù)

2.節(jié)流
每隔一段時間執(zhí)行一次

思路:
每次觸發(fā)事件時都判斷當前是否有等待執(zhí)行的延時函數(shù)

/**
 * underscore 節(jié)流函數(shù),返回函數(shù)連續(xù)調用時,func 執(zhí)行頻率限定為 次 / wait
 *
 * @param  {function}   func      回調函數(shù)
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始函數(shù)的的調用,傳入{leading: false}。
 *                                如果想忽略結尾函數(shù)的調用,傳入{trailing: false}
 *                                兩者不能共存,否則函數(shù)不能執(zhí)行
 * @return {function}             返回客戶調用函數(shù)
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的時間戳
    var previous = 0;
    // 如果 options 沒傳則設為空對象
    if (!options) options = {};
    // 定時器回調函數(shù)
    var later = function() {
      // 如果設置了 leading,就將 previous 設為 0
      // 用于下面函數(shù)的第一個 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
      // 如果需要第一次不執(zhí)行函數(shù)
      // 就將上次時間戳設為當前的
      // 這樣在接下來計算 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秒才觸發(fā),這時候就會進入這個條件
      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;
    };
  };
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。