前端開發工作中,我們經常在一個事件發生后執行某個操作,比如鼠標移動時打印一些東西:
1 window.addEventListener("mousemove", ()=>console.log(123)); 2 //測試發現鼠標移動了1毫米,回調函數執行了將近10次,這種做法是非常耗費資源的。
解決方法就是每進一個人都重新倒計時N秒再關門,這樣只要每個人都在前一個人進去N秒之內進門,那么每進一個人,電梯都會重新計時N秒,所以它只會在最后一個人進門N秒之后再啟動關門程序。
這里有三個關鍵點:「事件是高頻率發生的」、「在前一個發生后N秒內發生下一個」、「重新倒計時」。
函數防抖(debounce)就是以上解決方案的JavaScript實現,上面代碼的改寫思路是:每次事件發生后都只做兩件事——「清除舊的計時器」、「設置新的計時器」
//重置計時器的函數 function debounce(func, ms){ let timer = null; function reTimer(){ //重新計時 clearTimeout(timer) timer = setTimeout(func, ms) } return reTimer; } //要執行的動作 function handle(){ console.log("--- do something ---") } //綁定事件:每次鼠標移動時,就會執行debounce返回的reTimer函數 window.addEventListener("mousemove", debounce(handle, 1000))
前面說了,每次事件發生后都只做兩件事——「清除舊的計時器」、「設置新的計時器」,那么為什么要在addEventListener里執行debounce函數呢?
因為reTimer函數需要操作來自父級作用域的變量timer,而debounce函數就是為了創建這樣一個作用域,使得每次執行reTimer函數時timer變量都是存在的。
如果要求不使用debounce函數,我們就得把timer變量定義在addEventListener之前:
//重置倒計時 function reTimer(){ if(timer){ clearTimeout(timer) } timer = setTimeout(handle, 1000) } //事件處理 function handle(){ console.log("--- do something ---") } //事件綁定 let timer = null; //或者 window.timer = null window.addEventListener("mousemove", reTimer)
不過,相比使用debounce函數,這樣做就不那么優雅了。
addEventListener的目的是操作timer變量,而timer在debounce的作用域內,addEventListener訪問不到,所以用debounce返回的reTimer去訪問,這就是閉包了。
防抖是讓重復事件的處理函數只在最后一次發生時執行,而閉包只是一個更好的實現方案。
二、函數節流
理解了函數防抖,函數節流也就好辦了,我們只需要理解場景和方案。
假如我們正在做一個輸入框,要求每輸入一個字符都調用一個API來查詢數據,從而實現聯想、自動補全等功能,然而我們的輸入速度是很快的,可能還沒等第一個字符的查詢結果出來,第二個字符就已經敲進去了,所以我們需要讓查詢頻率小一點,具體做法就是在輸入的過程中,每隔N秒才查詢一次。
這里的關鍵點是:「事件是高頻率發生的」、「在前一個發生后N秒內發生下一個」、「一個計時結束后再重新計時」
定時器實現節流
節流函數(throttle)
/* * 節流函數生成器 * 傳遞事件處理函數和延遲時間 * 返回節流函數 */ function throttleGen(fn, delay) { let timer = null; function throller() { if (timer === null) { timer = setTimeout(function () { fn(); timer = null; }, delay) } } return throller; } //事件處理函數 function handle() { console.log('-- do something --'); } //綁定事件 window.addEventListener("mousemove", throttleGen(handle, 1000))
三、防抖和節流的對比
function debounce(fn, delay) { let timer = null; return function () { var _this = this; //這里改了 clearTimeout(timer); timer = setTimeout(function () { fn.apply(_this); //這里改了 }, delay); }; }