這種技術本質是一種優化,不是必需品,今天若只處在一個先求有再求好的環境時,比較不會考慮到這種技術,但假設已經有寫出基本的程式碼,後來想要節省效能,避免伺服器負擔過大的話,那這種技術就值得導入跟實際應用。

本次研究的主角:

Debounce,中文名字叫做防抖,我研究時在網路上有查詢到的定義如下,

1
2
無論用戶觸發多少次的事件,對應的回呼叫函數只會在事件停止觸發觸發指定事件後執行
也就是n秒後在執行該事件,若在n秒內被重複觸發,那就會重新計時。

Throttle,中文名字叫做節流,我研究時在網路上有查詢到的定義如下,

1
2
無論用戶觸發事件多少次,附加的函數在給定的時間見個內只會執行一次
n秒內只運行一次,若在n秒內重複觸發,那只會有一次生效。

防抖(Debounce)理解

我自己的理解會先從字面上解讀,防抖意思就是為防止抖動的意思,那思考什麼時候會不停抖動,我想像是使用者的手一直不停抖動,而這在操作網頁上會造成問題,假如今天有一個按一次買 100 個物品的按鈕,手抖的阿伯只想買 100 個,所以他想要按一次就好,但是他的手就是會抖個不停,所以在按個過程中一個不小心就多按了好幾下,結果就變成計算按好幾次的結果,但今天若是有防抖的機制,如上方定義所述:「無論用戶觸發多少次的事件,對應的回呼叫函數只會在事件停止觸發觸發指定事件後執行」,不管今天這個阿伯手抖按了多少下,短時間內這個按鈕只會被判斷按了一次,那這個阿伯手抖造成的危機就被「防抖」解決了。

節流(Throttle)理解

而節流將會更加直觀,節流節流,就是節省流量的意思,這個被利用在很多地方,像是現在大家最常用的 youtube 就有利用這個機制,現在點進去 youtube 就會發現說其實當前頁面並不是已經把所有資料(影片)載入的情況,它只會給一定量的影片,而要更多影片的時候,就會滑動到最下面,才會去加載更多的影片,這其實就是節流的核心概念,不要一開始就把所有資料都顯示出來,因為全部顯示出來也看不到那麼多,只要先把你目前夠看得顯示出來,其他的之後再顯示就好,藉由這種方式來節省流量。

比喻時間,用自動門來理解

自動門就是會自己打開跟關起來的門,如果不知道的可以去家裡附近的超商逛一下。

雖然生活中很常會碰到,但有沒有想過,這個自動門開跟關的時間呢?

自動門打開後太久沒有關,冷氣會跑光光;但打開馬上就關起來,又很容易造成夾人事件,所以說要設定一個合理的時間,既不會開起來太久,也不會關太多,這種自動門延遲時間多與寡的概念,其實就跟防抖很相似。

除了要適當的延遲時間外,還有一個防抖很重要的概念,自動門也擁有,就是上方所提到的:「若在 n 秒內被重複觸發,那就會重新計時」,在自動門來說的話,就是一個客人走進來之後,假如設定 5 秒鐘才關起來,那當過去 4 秒鐘後,第二個客人走進來這個還沒被關起的門時,不能讓它過去 1 秒直接關起來,而是應該再讓自動門獲得 5 秒才關起來的時間延遲,這種會因為重複觸發就會被重新計時的特性非常重要,可以說是防抖的核心。

節流的概念也可以用自動門來理解,只要把那個流想成人流就沒問題了。

假如有一個商店,因為疫情的關係發布了一項特別措施,要限制店裡不能同時進來太多人,也就是不管現在商店外大排長榮幾千人,商店進出的自動門被設定成 20 秒內只能進去一個人,所以說會有兩層自動門,在能一直確保後面有人的情況下,第一個客人走進去第一個自動門後,20 秒後會準時開啟一次,第二個客人才能走進去,這就是節流的概念。

做個重點比較:

自動門打開進來第一個客人後,等待 X 秒才關起來,在這 X 秒又有第二個客人進來,得重新計時 X 秒等待關閉,這是防抖(Debounce)。

自動門打開進來第一個客人後關閉,不管外面多少客人在等進來,每 X 秒後才會準時開啟一次,這是節流(Throttle)。

實作概念

現在很多框架都會把防抖(Debounce)及節流(Throttle)的語法包在裡面,所以其實只要會使用就好,不知道怎麼實作出來也可以使用,但其實原生 JS 實作的概念不困難。

上面看完後應該會發現,不論是防抖(Debounce)或是節流(Throttle)都跟延遲時間脫離不了關係,在原生 JS 中有一個跟延遲時間相關的語法,叫做setTimeout,它就是達成這兩者實作的核心概念。

防抖簡單版本:

1
2
3
4
5
6
7
8
9
10
function debounce(callback, time) {
let box;
return (...args) => {
clearTimeout(box);

box = setTimeout(() => {
callback(...args);
}, time);
};
}

debounce的函式會需要帶兩個參數,第一個是callback,意義是想要讓確保時間延遲完才去執行這個callback,第二個是 time,主要是控制要延遲多久時間這件事情。

除了setTimeout之外還會使用到clearTimeout,是因為這樣才能在重新呼叫 debounce 的函式時,可以把上一次的setTimeout給清除掉,達到每次觸發都會重新計時的效果。

另外可以發現我創造了一個叫做box的變數,因為這樣才能儲放setTimeoutclearTimeout才知道要把誰給清除掉,而這個地方也會利用到閉包 (Closure) 的特性,讓這個box的變數可以跟每個 debounce 的函式共享,假如都放在同一層的話,每次執行都會是新的,那就沒辦法做到新的setTimeout覆蓋舊的setTimeout這件事情。

節流簡單版本:

1
2
3
4
5
6
7
8
9
10
11
function throttle(callback, time) {
let box;
return () => {
if (box) return;

box = setTimeout(() => {
callback();
box = null;
}, time);
};
}

throttle的函式參數概念跟上方debounce的函式一樣,也會需要使用box的變數儲放setTimeout,但有一個很大的差別就是每一段時間只執行一次這點,所以不是重新觸發時去覆蓋一個新的setTimeout,而是當在時間內又重複觸發時,給它無效,這邊實作是判斷box的變數是不是存在(true),如果是的話就直接return

意味著上方定義所講的:「若在 n 秒內重複觸發,那只會有一次生效」,而在成功觸發後,這邊會直接去觸發要進行事件之後,再把box的變數給手動消除掉,這樣上方才能成功的判斷box的變數有無存在。

再來釐清一次順序,throttle的函式傳進去一個callbacktime後,可以想像是呼叫這個函式時就等於每幾秒去執行一次這個callback,假如今天是因為網站滑動到最底部的時候會觸發throttle,那第一次滑動到最底部時會判斷box的變數沒東西,所以會進到setTimeout裡面去執行callback及存入box的變數中,在這個setTimeout的時間還沒結束時,不管第幾次網站滑動到最底部,都會因為box的變數有東西而直接return不會有反應,藉此機制達成一段時間內只會執行一次。

實際應用

最後來整理一下這兩者的實際應用,有我自己平常會使用情境以及一些爬文研究時才發現的。

防抖除了前面有講到那個按按鈕的情況之外,我自己最常遇到的就是在輸入框的搜尋,像是我之前自己專案中有手做auto complete,那時候會就碰到說是打一個字就要發送一次請求,還是只需要最後一次輸入完,再發送請求,假如要用到後者就需要防抖。

另外還有兩種情況是我研究時看到的:

  • 自動保存,比如說像hackmd寫筆記輸入完,過一下下其實都會自動幫忙保存,避免忘記手動存東西不見。
  • 表單驗證檢查,有時候在輸入一些帳號密碼,或是信箱電話,有時候還沒送出只是格式寫錯,但過沒多久下方會冒紅色字提醒,其實也是防抖的應用。

節流的情境我最常用的也是當網頁中不會把全部資料一次載入完,可能最開始只會載入一部分,剩下的用功能搭配節流函式慢慢地顯示在網頁畫面上。

但有一種蠻特別的,也是節流的應用,就是玩遊戲時打擊的平 A,之前都沒有想到,但後來發現原來是用這種方式來實作的,在過去只知道攻速為 0.8 的話,代表 1 秒只能打 0.8 下,如果攻速為 2 時,1 秒可以打 2 下,但是最快也就如此,如果在這情況一秒點擊 1 下滑鼠,可以每秒打 1 下,但是一秒點擊 3 下滑鼠,也只能每秒打 2 下,之前只覺得這樣很合理,但現在回頭想想,確實使用到了節流的概念。

參考資料