前端優化中的Debounce及Throttle
這種技術本質是一種優化,不是必需品,今天若只處在一個先求有再求好的環境時,比較不會考慮到這種技術,但假設已經有寫出基本的程式碼,後來想要節省效能,避免伺服器負擔過大的話,那這種技術就值得導入跟實際應用。
本次研究的主角:
Debounce,中文名字叫做防抖,我研究時在網路上有查詢到的定義如下,
1 | 無論用戶觸發多少次的事件,對應的回呼叫函數只會在事件停止觸發觸發指定事件後執行 |
Throttle,中文名字叫做節流,我研究時在網路上有查詢到的定義如下,
1 | 無論用戶觸發事件多少次,附加的函數在給定的時間見個內只會執行一次 |
防抖(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 | function debounce(callback, time) { |
debounce
的函式會需要帶兩個參數,第一個是callback
,意義是想要讓確保時間延遲完才去執行這個callback
,第二個是 time,主要是控制要延遲多久時間這件事情。
除了setTimeout
之外還會使用到clearTimeout
,是因為這樣才能在重新呼叫 debounce 的函式時,可以把上一次的setTimeout
給清除掉,達到每次觸發都會重新計時的效果。
另外可以發現我創造了一個叫做box
的變數,因為這樣才能儲放setTimeout
,clearTimeout
才知道要把誰給清除掉,而這個地方也會利用到閉包 (Closure) 的特性,讓這個box
的變數可以跟每個 debounce 的函式共享,假如都放在同一層的話,每次執行都會是新的,那就沒辦法做到新的setTimeout
覆蓋舊的setTimeout
這件事情。
節流簡單版本:
1 | function throttle(callback, time) { |
throttle
的函式參數概念跟上方debounce
的函式一樣,也會需要使用box
的變數儲放setTimeout
,但有一個很大的差別就是每一段時間只執行一次這點,所以不是重新觸發時去覆蓋一個新的setTimeout
,而是當在時間內又重複觸發時,給它無效,這邊實作是判斷box
的變數是不是存在(true),如果是的話就直接return
。
意味著上方定義所講的:「若在 n 秒內重複觸發,那只會有一次生效」,而在成功觸發後,這邊會直接去觸發要進行事件之後,再把box
的變數給手動消除掉,這樣上方才能成功的判斷box
的變數有無存在。
再來釐清一次順序,throttle
的函式傳進去一個callback
跟time
後,可以想像是呼叫這個函式時就等於每幾秒去執行一次這個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 下,之前只覺得這樣很合理,但現在回頭想想,確實使用到了節流的概念。