iT邦幫忙

2022 iThome 鐵人賽

DAY 25
5

前言

這次要介紹的是 JS 實作 Debounce 和 Throttle,它們都有助於 JS 執行上效能的優化,怎麼說呢?

由於在網頁上進行滑鼠移動 (mousemove event) 或上下滾動視窗 (scroll event),又或是視窗縮放時,會觸發多次的 DOM 監聽事件,但其實也有可能只需要經過反覆調整後的最終畫面,那這樣短時間內觸發過多次的事件將會造成效能上的不佳,因此就需要 Debounce 和 Throttle 減少事件的觸發。


Debounce 防抖 介紹

透過 Debounce 處理過的函式,它會在某段時間內只執行觸發的最後一次事件,而且若事件不斷重新觸發,那就會不斷重新開始計時。

例如下圖,Debounce 設定的時間是 500 ms,若使用者瘋狂的點擊按鈕,就會不斷的重新計時,事件處理函式也就一直沒觸發,直到停下的 500 ms 後才觸發一次。

截圖的來源於 Debounce Vs Throttle: Definitive Visual Guide,有興趣的讀者也可以玩玩看。


Debounce 實作

了解 Debounce 後,我們來實作看看,加深對它的了解。

Step1

我們先將一些必要的函式加入,包括事件監聽和 Callback 函式。

function showLog(e) {
  console.log(e);
  console.log("hi");
}

document
  .getElementById("debounceBtn")
  .addEventListener("click", debounce(showLog, 1000));

debounce 會帶入兩個參數,一個就是要進行處理的函式 fn,另一個則是設定間隔多久觸發函式的時間 delay。

回傳的是一個函式,會根據設定的時間去觸發 fn 函式。

至於回傳的函式帶入參數,是為了處理 fn 所傳入的參數或是 event 物件。

function debounce(fn, delay) {
  return function(...args) {
    setTimeout(function() {
      fn.apply(this, args);
    }, delay)
  }
}

Step2

接著就是完成若事件不斷重新觸發,就會重新開始計時的功能,所以加上 timeId 的判斷機制。

這裡的 setTimeout 我改成了箭頭函式,避免 this 指向 window。

function debounce(fn, delay) {
  let timeId = null;

  return function(...args) {
    if (timeId) clearTimeout(timeId);

    timeId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

Step3

不過上個步驟完成的 debounce 在第一次呼叫時不會馬上觸發,我們希望在第一次觸發事件時也執行對應的函式,故修改成如下:

function debounce(fn, delay, immediate) {
  let timeId = null;

  return function(...args) {
    if (timeId) clearTimeout(timeId);
    
    if (immediate && !timeId) fn.apply(this, args);

    timeId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

到這邊 debounce 最主要的功能已經完成了,當然還有可以優化的地方,讀者也可以參考和閱讀 lodash 的 debounce

LeetCode 有出現實作 Debounce 的題目,看完上面內容的讀者也可以親自試試。


Throttle 節流 介紹

透過 Throttle 處理過的函式只會在間隔指定的時間執行,像範例圖中,雖然瘋狂的點擊按鈕,但它都只會在每隔 500 ms 才觸發一次事件處理。


Throttle 實作

了解 Throttle 後,我們來實作看看,加深對它的了解。

方式1

觀念其實蠻簡單的,主要就是利用時間戳記判斷是否已經間隔超過指定的時間了,如果是就執行函式。

function throttle(fn, delay) {
  let previousTime = 0;

  return function(...args) {
    const nowTime = new Date().getTime();

    if (nowTime - previousTime > delay) {
      fn.apply(this, args);
      previousTime = nowTime;
    }
  }
}

這種方式在事件觸發時會馬上執行函式,但例如 delay 設定 1 秒,而使用者在重複點擊按鈕 3.5 秒後停止,那第 4 秒時並不會執行函式。

方式2

當還有定時器時就不會繼續執行下一個函式,直到定時器被清除才會執行。

function throttle(fn, delay) {
  let timeId = null;

  return function(...args) {
    if (timeId) return; // 如果有計時器,表示還在 delay 的秒數內

    timeId = setTimeout(() => {
      timeId = null;
      fn.apply(this, args);
    }, delay);
  }
}

不過這種方式在事件觸發時不會馬上執行函式,但例如在重複點擊按鈕 3.5 秒後停止,會在 4 秒時再執行一次事件處理函式。

讀者可以將 delay 時間拉長,反覆觀察。

兩種方法合體

如果想要在一開始馬上執行,且在最後一次事件觸發時去處理事件處理函式的話,可以這樣改寫:

function throttle(fn, delay) {
  let timeId = null;
  let previousTime = 0;

  return function(...args) {
    const nowTime = new Date().getTime();
    const remain = delay - (nowTime - previousTime); // 下次觸發 fn 的時間
    
    if (remain <= 0 || remain > delay) { // 符合執行的條件
      if (timeId) timeId = null;
      
      previousTime = nowTime;
      fn.apply(this, args);
    } else if (!timeId) {
      timeId = setTimeout(() => {
        previousTime = new Date().getTime();
        timeId = null;
        fn.apply(this, args);
      }, remain)
    }
  }
}

本篇就介紹到這邊啦,這篇文章提到的範例程式在以下連結:
Debounce & Throttle JS Example

參考文章 & 推薦閱讀

Debounce Vs Throttle: Definitive Visual Guide

函数防抖(debounce)和节流(throttle)以及lodash的debounce源码赏析

Debouncing and Throttling in JavaScript: Comprehensive Guide

Decorators and forwarding, call/apply


上一篇
Day24-JavaScript 的 WeakSet & WeakMap 資料結構
下一篇
Day26-瞭解 JS 的淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

2
雷N
iT邦研究生 1 級 ‧ 2022-09-25 00:11:18

...

靠, 那兩張Gif好懂

以前剛碰RxJS時 也是在這兩個上有學習過

harry xie iT邦研究生 1 級 ‧ 2022-09-25 00:14:40 檢舉

好懂又好玩(多戳幾下/images/emoticon/emoticon01.gif

QQBoxy iT邦研究生 5 級 ‧ 2022-09-25 00:50:48 檢舉

真的,那兩張圖超棒Der

0
json_liang
iT邦研究生 4 級 ‧ 2022-09-25 00:14:00

感謝講解這兩個重要的概念

之前面試被問到這兩個概念都會搞混

harry xie iT邦研究生 1 級 ‧ 2022-09-25 00:19:57 檢舉

畢竟前端常碰到(連續輸入的功能就會用到)所以面試有時候會碰到

然後都是英文專有名詞的確容易搞混XD

我要留言

立即登入留言