花式解說防抖函數debounce的實現

歪式絮叨:本來打算只用一篇文章總結下防抖和節流的,可是寫着寫着發現挺有意思的。因此準備會分紅 3-4 篇來寫了,單獨的防抖和節流的實現,而後在去分享一下知名庫的源碼。今天先跟歪馬一塊兒看看防抖的實現吧。其餘內容敬請期待~!html


歪老師:「馬同窗,防抖和節流你知道嗎?起來講一下。」前端

馬同窗:「老師,我不知道呀,都沒據說過。」瀏覽器

歪老師:「好吧,那今天咱們就先來說講防抖吧。先從基本的概念以及使用場景提及」。服務器

1、概念

防抖 debounce 和節流 throttle 的概念並非 JS 所特有的。它們是在對函數持續調用時進行不一樣控制的兩個概念。今天咱們先介紹防抖。markdown

防抖是爲了不用戶無心間執行函數屢次。好比有些用戶喜歡在點擊的時候使用 double click 雙擊,或者就是手抖連續點了兩下,這就可能會致使一些意想不到的問題。函數

經過防抖能夠在事件觸發必定時間後沒有再次觸發同一事件時,再去執行相關的處理函數。oop

就比如你去菜市場買菜,到某個小攤上開始挑菜,接連挑好一袋又一袋放在攤主面前,攤主並不會每一袋都給你結帳,而是會等着問你:「還要別的嗎?」,等你確認完不要了,纔會結帳。性能

你能夠經過歪馬寫的這個 demo 查看常規無限制函數調用和防抖(節流)以後的可視化對比,完整 demo 地址以下: codesandbox.io/s/yibubupia…優化

2、做用&應用場景

在具體實現以前,咱們先簡單瞭解一下防抖和節流的做用以及在哪些業務中會用到。this

防抖和節流(這裏先包含它吧)主要可以給咱們帶來如下好處:

  1. 優化用戶體驗:適時反饋,避免 UI 渲染阻塞,瀏覽器卡頓
  2. 提高頁面性能:避免頁面渲染卡頓,減小服務器壓力,防範惡意觸發

防抖的應用場景有如下幾個方面:

  1. 輸入框內容聯想 --> 適時反饋、減小服務器壓力
  2. window.resize --> 避免 UI 渲染阻塞,瀏覽器卡頓
  3. 提交表單 --> 減小服務器壓力,防範惡意觸發

歪老師:「概念咱們就介紹到這,下面咱們來看看該如何實現。」

3、如何實現防抖

歪老師:「防抖能夠經過計時器來實現,經過setTimeout來指定必定時間後執行處理函數,若是在這以前事件再次觸發,則清空計時器,從新計時。」

function debounce(fn, wait{
  let timerId = null;
  return function(...args{
    if (timerId) clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn.call(this, args);
    }, wait);
  };
}
複製代碼

歪老師:「上面就是比較基礎的debounce功能的實現,同窗們都聽懂了吧?

馬牛羊同窗:「聽懂了。」

歪老師:「好,下面咱們來逐步拓展。馬同窗,在這個基礎上讓你去實現首次觸發時當即執行一次函數,你會怎麼實現?」

馬同窗:「老師,什麼場景下才須要在首次就執行呢?」

歪老師:「😓 這個你別管,如今要求就是這樣。你會怎麼實現?」

馬同窗:「哦,那讓我想一想。我知道了,若是timerIdnull時,直接執行就好了。」

function debounce(fn, wait) {
  let timerId = null;
  return function(...args) {
-   if (timerId) clearTimeout(timerId);
+   if (timerId) {
+     clearTimeout(timerId);
+   } else {
+     // 第一次直接調用函數
+     fn.call(this, args);
+   }
    timerId = setTimeout(() => {
      fn.call(this, args);
    }, wait);
  };
}
複製代碼

歪老師:「很好,這樣確實能實現首次觸發當即執行。但若是在通過正常的延遲執行(debounced 執行),中間又間隔了一段時間,再次觸發的時候,首次觸發會執行嗎?」

馬同窗:「呃,我想一想。好像是不會執行了。由於timerId一直有上次的值。就像下圖,中間應該有一次觸發的。若是要實現這一功能的話,能夠在每次延遲執行執行的時候將timerId置爲空。」

function debounce(fn, wait) {
  let timerId = null;
  return function(...args) {
    if (timerId) {
      clearTimeout(timerId);
    } else {
      fn.call(this, args);
    }
    timerId = setTimeout(() => {
      fn.call(this, args);
+     timerId = null;
    }, wait);
  };
}
複製代碼

歪老師:「不錯,一點就通,這樣確實能夠了。可是這樣的話,若是第一次延遲觸發和後面的新的觸發時間間隔小於咱們所設定的時間間隔。是否是也會觸發一次?若是想保持觸發間隔不小於 wait 事件間隔呢?」

馬同窗:「呃(⊙o⊙)…不知道,老師我以爲你故意刁難我。你咋不叫羊同窗他們」

歪老師:「哈哈,別這麼說,老師是在鍛鍊你的思考能力。這裏也能夠藉助相似上面的延時執行的思路。首次觸發是由timerId是否爲空決定的,要避免延遲執行以後的首次執行過早觸發,只要將上一步的置空操做也延時就好了。以下所示:」

function debounce(fn, wait) {
  let timerId = null;
  return function(...args) {
    if (timerId) {
      clearTimeout(timerId);
    } else {
      fn.call(this, args);
    }
    timerId = setTimeout(() => {
      fn.call(this, args);
+     setTimeout(() => {
        timerId = null;
+     }, wait);
    }, wait);
  };
}
複製代碼

歪老師:「那馬同窗,你以爲這樣會有什麼問題沒?」

馬同窗:「(思考片刻...)我以爲沒啥問題了。畢竟是老師你給出的方案」

歪老師:「不,這樣仍是有問題的。」

馬同窗:「老師,你這就是個連環套呀...一環扣一環。那還有什麼問題呀?」

歪老師:「咱們剛纔設置的延時置空定時器,並無 clear 的操做,因此在屢次連續觸發事件時,取消的操做其實按照第一次觸發的時間計算延時的,這就會致使首次執行在其後忽然觸發,而後首次執行的提早又會致使正常延時執行函數出問題(不會清計時器了),致使其按照首次執行上一次的來執行。以下圖所示:」

歪老師:「因此,這裏咱們只要在清空timerId的時候,將延時置空也取消就好了。

function debounce(fn, wait) {
  let timerId = null;
+ let leadingTimerId = null;
  return function(...args) {
    if (timerId) {
      clearTimeout(timerId);
+     clearTimeout(leadingTimerId);
    } else {
      fn.call(this, true, args);
    }
    timerId = setTimeout(() => {
      fn.call(this, false, args);
-     setTimeout(() => {
+     leadingTimerId = setTimeout(() => {
        timerId = null;
      }, wait);
    }, wait);
  };
}

複製代碼

歪老師:「如今馬同窗,你以爲還有問題嗎?」

馬同窗:「老師,我以爲應該還有問題。」

歪老師:「那有什麼問題呢?」

馬同窗:「額,老師,我就猜的,其實不知道...您再給說說」

歪老師:「就知道耍小聰明,不過確實仍是存在問題的。先看下圖。」

歪老師:「若是恰好只觸發了一次事件(能夠將 demo 裏的mousemove換爲click再試),會執行首次觸發,可是後續沒有其餘觸發,也會再觸發一次延時觸發。若是想避免這種問題,也就是若是首次觸發以後沒有再觸發,不進行延時觸發,應該怎麼作?」

歪老師:「馬同窗不用躲了,此次不問你了。老師直接說。」

function debounce(fn, wait{
  let timerId = null;
  let leadingTimerId = null;
  return function(...args{
    if (timerId) {
      clearTimeout(timerId);
      clearTimeout(leadingTimerId);

      timerId = setTimeout(() => {
        fn.call(thisfalse, args);
        leadingTimerId = setTimeout(() => {
          timerId = null;
        }, wait);
      }, wait);
    } else {
      fn.call(thistrue, args);
      // 爲了解決只觸發一次,會同時觸發首次觸發和延時觸發的問題引入的特殊值
      timerId = -1;
    }
  };
}
複製代碼

歪老師:「好了,今天就講這麼多,你們記一下課後做業。這樣還有沒有問題呢?你們能夠留言討論喲」。

總結

我知道這篇文章彷佛讀起來讓人暈暈乎乎的,而且你會發現,這和你想象中的防抖的實現彷佛並不同。可是這又怎樣呢?多數時候,咱們都是根據具體的使用場景去實現咱們須要的功能,因此重要的是要懂得如何去實現,同時也要隨機應變

而且最後你也知道了如何去實現 debounce,而且知道可能會有哪些坑了不是嗎?


相關連接已點亮到技能樹,歡迎查收

文檔連接:mubu.com/doc/4VGWywo…  密碼:歪碼行空


若是你喜歡,歡迎掃碼關注個人公衆號,我會按期陪讀,並分享一些其餘的前端知識喲。

相關文章
相關標籤/搜索