[JavaScript] 函式節流(throttle)和函式防抖(debounce)
js 的函式節流(throttle)和函式防抖(debounce)概述
函式防抖(debounce)
一個事件頻繁觸發,但是我們不想讓他觸發的這麼頻繁,於是我們就設定一個定時器讓這個事件在 xxx 秒之後再執行。如果 xxx 秒內觸發了,則清理定時器,重置等待事件 xxx 秒
比如在拖動 window 視窗進行 background 變色的操作的時候,如果不加限制的話,隨便拖個來回會引起無限制的頁面迴流與重繪
或者在使用者進行 input 輸入的時候,對內容的驗證放在使用者停止輸入的 300ms 後執行(當然這樣不一定好,比如銀行卡長度驗證不能再輸入過程中及時反饋)
一段程式碼實現視窗拖動變色
<script> let body = document.getElementsByTagName("body")[0]; let index = 0; if (body) { window.onresize = function() { index++; console.log("變色" + index + "次"); let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)]; body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")"; //晃瞎眼睛 }; } </script>
使用 setTimeout 進行延遲處理,每次觸發事件時都清除掉之前的方法
<script> let body = document.getElementsByTagName("body")[0]; let index = 0; let timer = null; if (body) { window.onresize = function() { //如果在一秒的延遲過程中再次觸發,就將定時器清除,清除完再重新設定一個新的 clearTimeout(timer); timer = setTimeout(function() { index++; console.log("變色" + index + "次"); let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)]; body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")"; }, 1000); //反正就是等你什麼都不幹一秒後才會執行程式碼 }; } </script>
但是目前有一個問題,就是程式碼耦合,這樣不夠優雅(b 格),將防抖和變色分離一下
<script> let body = document.getElementsByTagName("body")[0]; let index = 0; let lazyLayout = debounce(changeBgColor, 1000); window.onresize = lazyLayout; function changeBgColor() { index++; console.log("變色" + index + "次"); let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)]; body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")"; } //函式去抖(連續事件觸發結束後只觸發一次) function debounce(func, wait) { let timeout, context, args; //預設都是undefined return function() { context = this; args = arguments; if (timeout) clearTimeout(timeout); timeout = setTimeout(function() { //執行的時候到了 func.apply(context, args); }, wait); }; } </script>
當然 debounce 在一些有成熟的實現,underscore.js 的 debounce
//1.9.1 _.debounce = function(func, wait, immediate) { var timeout, result; var later = function(context, args) { timeout = null; if (args) result = func.apply(context, args); }; var debounced = restArguments(function(args) { if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(later, wait); if (callNow) result = func.apply(this, args); } else { timeout = _.delay(later, wait, this, args); } return result; }); debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; };
函式節流(throttle)
一個事件頻繁觸發,但是在 xxx 秒內只能執行一次程式碼
//上面的變色在節流中就是這樣寫了 <script> let doSomething = true; let body = document.getElementsByTagName("body")[0]; let index = 0; window.onresize = function() { if (!doSomething) return; doSomething = false; setTimeout(function() { index++; console.log("變色" + index + "次"); let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)]; body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")"; doSomething = true; }, 1000); }; </script>
跟上面的防抖差不多,隨便分離一下,降低程式碼的耦合度
<script> let body = document.getElementsByTagName("body")[0]; let index = 0; let lazyLayout = throttle(changeBgColor, 1000); window.onresize = lazyLayout; function changeBgColor() { index++; console.log("變色" + index + "次"); let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)]; body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")"; } //函式去抖(連續事件觸發結束後只觸發一次) function throttle(func, wait) { let context, args, doSomething = true; return function() { context = this; args = arguments; if (!doSomething) return; doSomething = false; setTimeout(function() { //執行的時候到了 func.apply(context, args); doSomething = true; }, wait); }; } </script>
underscore 中關於 throttle 的實現
_.throttle = function(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : _.now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function() { var now = _.now(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; };
防抖和節流是用來幹啥的?
防抖
- 繫結 scroll 滾動事件,resize 監聽事件
- 滑鼠點選,執行一個非同步事件,相當於讓使用者連續點選事件只生效一次(很有用吧)
- 還有就是輸入框校驗事件(但是不一定好使,比如校驗銀行卡長度,當你輸入完之後已經超出 100 個字元,正常應該是超出就提示錯誤資訊)
節流
- 當然還是滑鼠點選啦,但是這個是限制使用者點選頻率。類似於你拿把 ak47 射擊,槍的射速是 100 發/分鐘,但是的手速達到 1000 按/分鐘,就要限制一下嘍(防止惡意刷子)
- 載入更多的功能
其實二者主要就是為了解決短時間內連續多次重複觸發和大量的 DOM 操作的問題,來進行效能優化(重點是同時還能接著辦事,並不耽誤)
防抖主要是一定在 xxx 秒後執行,而節流主要是在 xxx 內執行(時間之後,時間之內)
文章寫的時候用的 underscore 1.8.2 版本,實現也是參考 underscore 的原始碼,實現方式與 underscore 最新有些程式碼還是不太一樣了。(功能還是相同的)