# [JavaScript] debounce 及 throttle ###### tags: `前端筆記` ## 這兩個的功用是是什麼? 兩個都是藉由 `closure`(閉包)實作的優化觸發事件的手段,以減少重複觸發事件,避免短時間重複觸發事件,造成重複執行不需要的行為(如重複執行 API call 的行為)。 雖然兩個都是優化的方法,但是各自有各自適合使用的地方。 ## debounce(防抖) 將多次的觸發事件的行為,透過記錄及 `timeout` 待最後一次觸發才真的執行任務。 > 適用情境:input 輸入搜尋(不可能使用者輸入一個字就直接打請求,這樣子當輸入十個字就發送十次請求了 => 但這個情境下我們只需發送使用者最後一次輸入時的關鍵字執行 API call 即可) ### 需要知道的資訊 - 怎麼知道使用者是最後一次輸入? - 用 `setTimeout` 丟一個計時器,當計時器到了以前事件(`input`)未被觸發,代表使用者已經結束輸入 - 那要怎麼記錄這個計時器? - 透過閉包(閉包即是函式在語彙範疇之外被叫用,也可以讀取該函式擁有的語彙範疇的能力) - 那要怎麼讓任務(需要執行的函式)可以跟計時器連結? - 在 JavaScript 中函式是 first-class-citizen(頭等公民),代表函式可以當作任何東西(如變數保存的指向,甚至是另一個函式的 parameter) - 把要執行的任務也丟進來,透過 parameter 放入計時器內執行 - 要怎麼正確地保存計時器? - 不可能由事件建立,要不然每一次事件觸發都會建立一個新的計時器,就不會有優化的效果 - 所以才會透過 `closure`(閉包)及 HOF 先建立一個函式,並且保存計時器 ### 程式範例 ```javascript= const inputEle = document.querySelector('#test-input') const displayDebouncedVal = document.querySelector('#debounced-value') /* Step 1: 建立好 debouncedFunc HOF 這個函式接受實際執行任務的 callback,及計時的時間 */ const debouncedFunc = (callback, delay = 2000) => { /* 記錄計時器的變數 */ let timer = null /* * 回傳另一個函式,這個函式控制什麼時候可以執行任務 callback * - 透過 speard operator 讀取箭頭函式的參數(會將輸入的 parameters 按照順序並回傳 array like []) * - 因為範疇是往內找,所以回傳的內函式可以讀取 timer 變數 * */ return (...args) => { /* 如果有 timer 代表使用者還在輸入,就把上一次建立的 timer 清掉 */ if (timer) { clearTimeout(timer) } /* 參照當前輸入建立 timer */ timer = setTimeout(() => { /* 注意,這裡必須用 speard oparator 展開 ...args,要不然任務的 callback 會收到 array like [] 的 parameter */ callback(...args) }, delay) } } /* * Step 2: 叫用 debouncedFunc 並且帶入實際要執行任務的 callback 及計時時間(2 秒) * - 此時 debouncedInput 保存的是 debouncedFunc 回傳的函式 * */ const debouncedInput = debouncedFunc((val) => { displayDebouncedVal.textContent = val }, 2000) /* * Step 3: 綁定事件,並且拿事件的輸入值帶進 debouncedInput * */ inputEle.addEventListener('input', (e) => { debouncedInput(e.target.value) }) ``` ### 實際執行的流程 ```javascript= 1. 建立好 `debounced` 函式(內有計時器及記錄 `timer`) 2. 將任務丟入 `debounced` 函式 3. 當使用者輸入時 - 會建立 timer - 清除上一個 timer - loop - 直到使用者結束輸入 - 最後一個計時器倒數完畢,執行計時器的任務 ``` ### 完整的程式範例 - [[Practice] Debounce with vanilla JS](https://codepen.io/lun0223/pen/jOXMqdE) - [[Practice] Debounce with react](https://codesandbox.io/s/practice-debounce-with-react-y3h7qs) ## throttle(節流) 與 debounced 不同,throttle 的目的是要讓使用者完成觸發條件後,於一定的時間內不重複觸發(所以不是記錄使用者什麼使用結束才真的觸發,而是當第一次觸發後就強迫使用者等待一定時間後才可觸發)。 > 適用情境:如滾動載入,當使用者滾動至一定的位置後,在一定的時間點內不再打 API。 ### 需要知道的資訊 - 要怎麼知道在一定的時間點內都不執行? - 透過閉包保存計時器,並且判斷計時器是否存在,若存在就不執行 - `timer = setTimeout(() => { timer = null })` 計時器內更動保存計時器的變數是有效的 ### 程式範例 可以發現與 debounce 很像,都是用 `closure`(達成記錄計時器的手段): ```javascript= const inputEle = document.querySelector('#test-input') const throttleValueEle = document.querySelector('#throttle-value') /* Step 1: 建立好 throttledFunc HOF 這個函式接受實際執行任務的 callback,及計時的時間 */ const throttledFunc = (callback, delay = 5000) => { let timer = null /* * 回傳另一個函式 * - 這個函式控制在什麼時間之內不執行 callback * */ return (...args) => { /* 第一次事件觸發後就會有計時器,所以有計時器 = 時間未到,所以不執行任務 callback */ if (timer) return timer = setTimeout(() => { /* 一樣展開 ...args */ callback(...args) /* 執行任務後洗掉 timer,供使用者下次一觸發事件後阻擋 */ timer = null }, delay) } } /* * Step 2: 叫用 throttledFunc 並且帶入實際要執行任務的 callback 及計時時間(2 秒) * - 此時 throlltedInputFunc 保存的是 throttledFunc 回傳的函式 * */ const throlltedInputFunc = throttledFunc((value) => { throttleValueEle.textContent = value }, 2000) /* * Step 3: 綁定事件,並且拿事件的輸入值帶進 debouncedInput * - 注意:因為使用者觸發事件就會執行任務,但是有計時器的關係,所以實際計時器取得的資料與最新的資料會有落差(因為 scope 不一樣) * */ inputEle.addEventListener('input', (e) => { throlltedInputFunc(e.target.value) }) ``` ### 實際執行的流程 ```javascript= 1. 建立好 `throttledFunc` 函式(內有計時器及記錄 `timer`) 2. 將任務丟入 `throttledFunc` 函式 3. 當使用者輸入時 - 會建立 timer - 待該 timer 執行前都不會執行任務(即便使用者一直打字也是一樣) - 計時器倒數完畢,執行任務,因為 `scope` 的不同,任務 callback 內的資料也許會和當前的資料不同 ``` ### 完整的程式範例 - [[Practice] Throttle with vanilla JS](https://codepen.io/lun0223/pen/RwEGRPE) ## 參考資料 1. [手寫節流 (throttle) 函式](https://www.explainthis.io/zh-hant/swe/throttle) 2. [手寫防抖 (debounce) 函式](https://www.explainthis.io/zh-hant/swe/debounce)