【進階 7-2 期】深刻淺出防抖函數 debounce

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端

------ 如下是正文 ------git

引言

上一節咱們認識了節流函數 throttle,瞭解了它的定義、實現原理以及在 underscore 中的實現。這一小節會繼續以前的篇幅聊聊防抖函數 debounce,結構是同樣的,將分別介紹定義、實現原理並給出了 2 種實現代碼並在最後介紹在 underscore 中的實現,歡迎你們拍磚。github

有什麼想法或者意見均可以在評論區留言,下圖是本文的思惟導圖,高清思惟導圖和更多文章請看個人 Github面試

111

定義及解讀

防抖函數 debounce 指的是某個函數在某段時間內,不管觸發了多少次回調,都只執行最後一次。假如咱們設置了一個等待時間 3 秒的函數,在這 3 秒內若是遇到函數調用請求就從新計時 3 秒,直至新的 3 秒內沒有函數調用請求,此時執行函數,否則就以此類推從新計時。緩存

img

舉一個小例子:假定在作公交車時,司機需等待最後一我的進入後再關門,每次新進一我的,司機就會把計時器清零並從新開始計時,從新等待 1 分鐘再關門,若是後續 1 分鐘內都沒有乘客上車,司機會認爲乘客都上來了,將關門發車。性能優化

此時「上車的乘客」就是咱們頻繁操做事件而不斷涌入的回調任務;「1 分鐘」就是計時器,它是司機決定「關門」的依據,若是有新的「乘客」上車,將清零並從新計時;「關門」就是最後須要執行的函數。閉包

若是你還沒法理解,看下面這張圖就清晰多了,另外點擊 這個頁面 查看節流和防抖的可視化比較。其中 Regular 是不作任何處理的狀況,throttle 是函數節流以後的結果(上一小節已介紹),debounce 是函數防抖以後的結果。app

image-20190525193539745

原理及實現

實現原理就是利用定時器,函數第一次執行時設定一個定時器,以後調用時發現已經設定過定時器就清空以前的定時器,並從新設定一個新的定時器,若是存在沒有被清空的定時器,當定時器計時結束後觸發函數執行。前端性能

實現 1

// 實現 1
// fn 是須要防抖處理的函數
// wait 是時間間隔
function debounce(fn, wait = 50) {
    // 經過閉包緩存一個定時器 id
    let timer = null
    // 將 debounce 處理結果看成函數返回
    // 觸發事件回調時執行這個返回函數
    return function(...args) {
      	// 若是已經設定過定時器就清空上一次的定時器
        if (timer) clearTimeout(timer)
      
      	// 開始設定一個新的定時器,定時器結束後執行傳入的函數 fn
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, wait)
    }
}

// DEMO
// 執行 debounce 函數返回新函數
const betterFn = debounce(() => console.log('fn 防抖執行了'), 1000)
// 中止滑動 1 秒後執行函數 () => console.log('fn 防抖執行了')
document.addEventListener('scroll', betterFn)
複製代碼

實現 2

上述實現方案已經能夠解決大部分使用場景了,不過想要實現第一次觸發回調事件就執行 fn 有點力不從心了,這時候咱們來改寫下 debounce 函數,加上第一次觸發當即執行的功能。函數

// 實現 2
// immediate 表示第一次是否當即執行
function debounce(fn, wait = 50, immediate) {
    let timer = null
    return function(...args) {
        if (timer) clearTimeout(timer)
      
      	// ------ 新增部分 start ------ 
      	// immediate 爲 true 表示第一次觸發後執行
      	// timer 爲空表示首次觸發
        if (immediate && !timer) {
            fn.apply(this, args)
        }
      	// ------ 新增部分 end ------ 
      	
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, wait)
    }
}

// DEMO
// 執行 debounce 函數返回新函數
const betterFn = debounce(() => console.log('fn 防抖執行了'), 1000, true)
// 第一次觸發 scroll 執行一次 fn,後續只有在中止滑動 1 秒後才執行函數 fn
document.addEventListener('scroll', betterFn)
複製代碼

實現原理比較簡單,判斷傳入的 immediate 是否爲 true,另外須要額外判斷是不是第一次執行防抖函數,判斷依舊就是 timer 是否爲空,因此只要 immediate && !timer 返回 true 就執行 fn 函數,即 fn.apply(this, args)

增強版 throttle

如今考慮一種狀況,若是用戶的操做很是頻繁,不等設置的延遲時間結束就進行下次操做,會頻繁的清除計時器並從新生成,因此函數 fn 一直都沒辦法執行,致使用戶操做遲遲得不到響應。

有一種思想是將「節流」和「防抖」合二爲一,變成增強版的節流函數,關鍵點在於「 wait 時間內,能夠從新生成定時器,但只要 wait 的時間到了,必須給用戶一個響應」。這種合體思路剛好能夠解決上面提出的問題。

給出合二爲一的代碼以前先來回顧下 throttle 函數,上一小節中有詳細的介紹。

// fn 是須要執行的函數
// wait 是時間間隔
const throttle = (fn, wait = 50) => {
  // 上一次執行 fn 的時間
  let previous = 0
  // 將 throttle 處理結果看成函數返回
  return function(...args) {
    // 獲取當前時間,轉換成時間戳,單位毫秒
    let now = +new Date()
    // 將當前時間和上一次執行函數的時間進行對比
    // 大於等待時間就把 previous 設置爲當前時間並執行函數 fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}
複製代碼

結合 throttle 和 debounce 代碼,增強版節流函數 throttle 以下,新增邏輯在於當前觸發時間和上次觸發的時間差小於時間間隔時,設立一個新的定時器,至關於把 debounce 代碼放在了小於時間間隔部分。

// fn 是須要節流處理的函數
// wait 是時間間隔
function throttle(fn, wait) {
  
  // previous 是上一次執行 fn 的時間
  // timer 是定時器
  let previous = 0, timer = null
  
  // 將 throttle 處理結果看成函數返回
  return function (...args) {
    
    // 獲取當前時間,轉換成時間戳,單位毫秒
    let now = +new Date()
    
    // ------ 新增部分 start ------ 
    // 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔
    if (now - previous < wait) {
    	 // 若是小於,則爲本次觸發操做設立一個新的定時器
       // 定時器時間結束後執行函數 fn 
       if (timer) clearTimeout(timer)
       timer = setTimeout(() => {
          previous = now
        	fn.apply(this, args)
        }, wait)
    // ------ 新增部分 end ------ 
      
    } else {
       // 第一次執行
       // 或者時間間隔超出了設定的時間間隔,執行函數 fn
       previous = now
       fn.apply(this, args)
    }
  }
}

// DEMO
// 執行 throttle 函數返回新函數
const betterFn = throttle(() => console.log('fn 節流執行了'), 1000)
// 第一次觸發 scroll 執行一次 fn,每隔 1 秒後執行一次函數 fn,中止滑動 1 秒後再執行函數 fn
document.addEventListener('scroll', betterFn)
複製代碼

看完整段代碼會發現這個思想和上篇文章介紹的 underscore 中 throttle 的實現思想很是類似。

underscore 源碼解析

看完了上文的基本版代碼,感受仍是比較輕鬆的,如今來學習下 underscore 是如何實現 debounce 函數的,學習一下優秀的思想,直接上代碼和註釋,本源碼解析依賴於 underscore 1.9.1 版本實現。

// 此處的三個參數上文都有解釋
_.debounce = function(func, wait, immediate) {
  // timeout 表示定時器
  // result 表示 func 執行返回值
  var timeout, result;

  // 定時器計時結束後
  // 一、清空計時器,使之不影響下次連續事件的觸發
  // 二、觸發執行 func
  var later = function(context, args) {
    timeout = null;
    // if (args) 判斷是爲了過濾當即觸發的
    // 關聯在於 _.delay 和 restArguments
    if (args) result = func.apply(context, args);
  };

  // 將 debounce 處理結果看成函數返回
  var debounced = restArguments(function(args) {
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      // 第一次觸發後會設置 timeout,
      // 根據 timeout 是否爲空能夠判斷是不是首次觸發
      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;
};

// 根據給定的毫秒 wait 延遲執行函數 func
_.delay = restArguments(function(func, wait, args) {
  return setTimeout(function() {
    return func.apply(null, args);
  }, wait);
});
複製代碼

相比上文的基本版實現,underscore 多瞭如下幾點功能。

  • 一、函數 func 的執行結束後返回結果值 result
  • 二、定時器計時結束後清除 timeout,使之不影響下次連續事件的觸發
  • 三、新增了手動取消功能 cancel
  • 四、immediate 爲 true 後只會在第一次觸發時執行,頻繁觸發回調結束後不會再執行

小結

  • 函數節流和防抖都是「閉包」、「高階函數」的應用

  • 函數節流 throttle 指的是某個函數在必定時間間隔內(例如 3 秒)執行一次,在這 3 秒內 無視後來產生的函數調用請求

    • 節流能夠理解爲養金魚時擰緊水龍頭放水,3 秒一滴
      • 「管道中的水」就是咱們頻繁操做事件而不斷涌入的回調任務,它須要接受「水龍頭」安排
      • 「水龍頭」就是節流閥,控制水的流速,過濾無效的回調任務
      • 「滴水」就是每隔一段時間執行一次函數
      • 「3 秒」就是間隔時間,它是「水龍頭」決定「滴水」的依據
    • 應用:監聽滾動事件添加節流函數後,每隔固定的一段時間執行一次
    • 實現方案 1:用時間戳來判斷是否已到執行時間,記錄上次執行的時間戳,而後每次觸發後執行回調,判斷當前時間距離上次執行時間的間隔是否已經達到時間差(Xms) ,若是是則執行,並更新上次執行的時間戳,如此循環
    • 實現方案 2:使用定時器,好比當 scroll 事件剛觸發時,打印一個 hello world,而後設置個 1000ms 的定時器,此後每次觸發 scroll 事件觸發回調,若是已經存在定時器,則回調不執行方法,直到定時器觸發,handler 被清除,而後從新設置定時器
  • 函數防抖 debounce 指的是某個函數在某段時間內,不管觸發了多少次回調,都只執行最後一次

    • 防抖能夠理解爲司機等待最後一我的進入後再關門,每次新進一我的,司機就會把計時器清零並從新開始計時
      • 「上車的乘客」就是咱們頻繁操做事件而不斷涌入的回調任務
      • 「1 分鐘」就是計時器,它是司機決定「關門」的依據,若是有新的「乘客」上車,將清零並從新計時
      • 「關門」就是最後須要執行的函數
    • 應用:input 輸入回調事件添加防抖函數後,只會在中止輸入後觸發一次
    • 實現方案:使用定時器,函數第一次執行時設定一個定時器,以後調用時發現已經設定過定時器就清空以前的定時器,並從新設定一個新的定時器,若是存在沒有被清空的定時器,當定時器計時結束後觸發函數執行

參考

underscore.js

前端性能優化原理與實踐

文章穿梭機

❤️ 看完三件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-
  2. 關注個人 GitHub,讓咱們成爲長期關係
  3. 關注公衆號「高級前端進階」,每週重點攻克一個前端面試重難點,公衆號後臺回覆「資料」 送你精選前端優質資料。

相關文章
相關標籤/搜索