1.前言
在一次面試中被問到:“談一談js中函數的防抖和節流。”,當時菜雞如我的內心:
只能弱弱的說一句沒怎么了解過。后來找到工作后就將這件事拋在腦后,也沒在深究。
就在前幾天維護公司內部代碼的時候,發現這樣一個場景:當用戶在創建東西時,會把用戶輸入的名字發往服務端校驗是否重名,而當時的代碼是監聽了input
輸入框的onchange
事件,只要用戶一輸入字符,就立即發出請求校驗,這能忍?如果名字有100個字符發100次請求?用戶沒輸完你校驗個毛線啊!
不能忍!優化!必須優化!首先想到的優化思路是:當用戶輸完后我再發請求校驗,但是我又不知道用戶什么時候輸完。那么可以這樣,用戶一直在輸入時,我不請求,當用戶停止輸入3秒后我就認為此時用戶已經輸入完成,這時候再發請求校驗,這樣即可大大的降低請求次數,提高性能。
就在我沾沾自喜的拿着優化方案給Leader看的時候,Leader聽完淡淡的說了一句:函數防抖和節流了解一下。
此時回過神來,原來這就是防抖啊。
2.概念
函數防抖和節流,都是控制事件觸發頻率的方法,通常用戶優化性能。
2.1 函數防抖(debounce)最后一個人說了算
函數防抖,就是指觸發事件后在
n
秒內函數只能執行一次,如果在n
秒內又觸發了事件,則會重新計算函數執行時間。
函數防抖,這里的抖動就是執行的意思,而一般的抖動都是持續的,多次的。假設函數持續多次執行,我們希望讓它冷靜下來再執行。也就是當持續觸發事件的時候,函數是完全不執行的,等最后一次觸發結束的一段時間之后,再去執行。
防抖的中心思想在於:我會等你到底。在某段時間內,不管你觸發了多少次回調,我都只認最后一次。
簡單的說,當一個動作連續觸發,則只執行最后一次。
常見應用場景:
連續的事件,只需觸發一次回調的場景有:
- 搜索框搜索輸入。只需用戶最后一次輸入完,再發送請求
- 手機號、郵箱驗證輸入檢測
- 窗口大小Resize。只需窗口調整完成后,計算窗口大小。防止重復渲染。
2.2 函數節流(throttle) 第一個人說了算
函數節流,就是限制一個函數在一定時間內只能執行一次。
節流的意思是讓函數有節制地執行,而不是毫無節制的觸發一次就執行一次。什么叫有節制呢?就是在一段時間內,只執行一次。
節流中心思想在於:在某段時間內,不管你觸發了多少次回調,我都只認第一次,並在計時結束時給予響應。
常見應用場景:
間隔一段時間執行一次回調的場景有:
- 滾動加載,加載更多或滾到底部監聽
- 谷歌搜索框,搜索聯想功能
- 高頻點擊提交,表單重復提交
2.3 直觀理解
為了方便理解,我們首先通過畫圖感受一下三種環境(正常情況、函數防抖情況 debounce
、函數節流 throttle
)下,對於mousemove
事件回調的執行情況。
豎線的疏密代表事件執行的頻繁程度。可以看到,正常情況下,豎線非常密集,函數執行的很頻繁。而debounce
(函數防抖)則很稀疏,只有當鼠標停止移動時才會執行一次。throttle
(函數節流)分布的較為均已,每過一段時間就會執行一次。
3.代碼實現
為了說明問題,假設一個場景:鼠標滑過一個div,觸發onmousemove事件,它內部的文字會顯示當前鼠標的坐標。
<style>
#box {
width: 1000px;
height: 500px;
background: #ccc;
font-size: 40px;
text-align: center;
line-height: 500px;
}
</style>
<div id="box"></div>
<script>
const box = document.getElementById('box')
box.onmousemove = function (e) {
box.innerHTML = `${e.clientX}, ${e.clientY}`
}
</script>
效果如下:
3.1 函數防抖(debounce)
我們想要這樣的效果:當鼠標持續移動時,不顯示鼠標坐標,當鼠標停止移動1秒后再顯示鼠標坐標。
分解一下需求:
- 持續觸發不執行
- 不觸發的一段時間之后再執行
那么怎么實現上述的目標呢?我們先看這一點:在不觸發的一段時間之后再執行,那就需要個定時器呀,定時器里面調用我們要執行的函數,將arguments傳入。
封裝一個函數,讓持續觸發的事件監聽是我們封裝的這個函數,將目標函數作為回調(func)傳進去,等待一段時間過后執行目標函數。
function debounce(func, delay) {
return function() {
setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}
第二點實現了,再看第一點:持續觸發不執行。我們先思考一下,是什么讓我們的函數執行了呢?是上邊的setTimeout。OK,那現在的問題就變成了持續觸發,不能有setTimeout。這樣直接在事件持續觸發的時候,清掉定時器就好了。
// func是我們需要包裝的事件回調, delay是每次推遲執行的等待時間
function debounce(func, delay) {
// 定時器
let timeout = null;
return function() {
// 每次事件被觸發時,都去清除之前的舊定時器,舊定時器的回調就不會執行。
if(timer) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}
用法:
box.onmousemove = debounce(function (e) {
box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)
效果:
說明:
這里debounce函數執行的結果是其內部return的function的調用。也就是說鼠標經過的事件監聽實際上是這個被return的function,不斷持續觸發的是它,而debounce函數內部用閉包聲明了一個timeout的定時器,由於閉包的存在,timeout會被掛載在window對象上,每次鼠標經過,都會先清除掉上次聲明的timeout,直到最后一次鼠標經過,而它的timeout沒有被清除,所以最后一次的定時器才會執行。
3.2 函數節流(throttle)
我們想要這樣的效果:當鼠標持續移動時,不顯示鼠標坐標,每隔一定的時間再顯示鼠標坐標。
同樣,我們再分解一下需求:
- 持續觸發並不會執行多次
- 到一定時間再去執行
持續觸發,並不會執行,但是到時間了就會執行。抓取一個關鍵的點:就是執行的時機。要做到控制執行的時機,我們可以通過一個開關,與定時器setTimeout結合完成。
函數執行的前提條件是開關打開,持續觸發時,持續關閉開關,等到setTimeout到時間了,再把開關打開,函數就會執行了。
function throttle(func, delay) {
let run = true
return function () {
if (!run) {
return // 如果開關關閉了,那就直接不執行下邊的代碼
}
run = false // 持續觸發的話,run一直是false,就會停在上邊的判斷那里
setTimeout(() => {
func.apply(this, arguments)
run = true // 定時器到時間之后,會把開關打開,我們的函數就會被執行
}, delay)
}
}
用法:
box.onmousemove = throttle(function (e) {
box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)
效果:
4.總結
防抖和節流巧妙地用了setTimeout,來控制函數執行的時機,優點很明顯,可以節約性能,不至於多次觸發復雜的業務邏輯而造成頁面卡頓。
函數防抖,在一段連續操作結束后,處理回調,利用 clearTimeout 和 setTimeout 實現。函數節流,在一段連續操作中,每一段時間只執行一次,頻率較高的事件中使用來提高性能。
函數防抖關注一定時間連續觸發,只在最后執行一次,而函數節流側重於一段時間內只執行一次。
(完)