什么是節(jié)流防抖?
在用戶瀏覽網(wǎng)頁(yè)或者在網(wǎng)頁(yè)上進(jìn)行一些操作時(shí),我們常常需要監(jiān)聽(tīng)一些事件去完成相應(yīng)的功能。比如鼠標(biāo)點(diǎn)擊、鍵盤(pán)輸入、滾輪滾動(dòng)等等一些事件。
在處理這些事件時(shí),我們可能會(huì)發(fā)現(xiàn),某些事件發(fā)生的頻率特別高,有的事件監(jiān)聽(tīng)函數(shù)甚至直接造成了頁(yè)面的卡頓和崩潰。
舉個(gè)例子,假如我們需要在用戶輸入時(shí),根據(jù)用戶的輸入內(nèi)容自動(dòng)聯(lián)想相關(guān)詞匯進(jìn)行搜索:
const input = document.querySelector('input')
input.addEventListener('input', () => {
console.log(1)
})
此時(shí)可以看到這個(gè)效果:

用戶每輸入一個(gè)字符就進(jìn)行一次相關(guān)操作,有時(shí)候很可能是還沒(méi)有輸入完成或者輸錯(cuò)的情況,但我們的代碼也進(jìn)行了一次操作,這就造成了一些不必要的網(wǎng)絡(luò)請(qǐng)求和性能浪費(fèi)。
所以,我們并不需要代碼如此高頻率的執(zhí)行,這在一定程度上對(duì)系統(tǒng)資源造成了浪費(fèi),程序的性能也會(huì)因此變得很差。因此,我們需要對(duì)這些事件的監(jiān)聽(tīng)做更進(jìn)一步的處理,對(duì)代碼的執(zhí)行頻率進(jìn)行一些限制。這就是所謂的節(jié)流防抖。
節(jié)流防抖的原理
節(jié)流和防抖,二者的作用雖然相同,但是在原理和實(shí)現(xiàn)上存在一定的區(qū)別:
節(jié)流:頻繁地觸發(fā)某個(gè)事件,但一定的時(shí)間內(nèi)只執(zhí)行一次。函數(shù)名通常為 throttle。
防抖:頻繁地觸發(fā)某個(gè)事件,但只執(zhí)行最后一次。函數(shù)名通常為 debounce。
實(shí)現(xiàn)節(jié)流函數(shù)
雖然事件不斷地被觸發(fā),但節(jié)流函數(shù)就就像紅綠燈一樣,不管前面有多少車(chē)輛排隊(duì),我就是需要等待一段時(shí)間才變?yōu)榫G燈,你才能通行。
所以最直接的方式還是利用定時(shí)器去處理。當(dāng)事件首次被觸發(fā)時(shí),開(kāi)始計(jì)時(shí) wait 毫秒,wait 毫秒以內(nèi)不管觸發(fā)了多少次,都會(huì)被忽略,直到計(jì)時(shí)結(jié)束后再去執(zhí)行。
思路有了,方法就不難實(shí)現(xiàn)。將事件處理函數(shù)設(shè)為第一個(gè)參數(shù) func,將等待時(shí)間設(shè)為第二個(gè)參數(shù) wait。
// 節(jié)流函數(shù)
function throttle(func, wait) {
// 創(chuàng)建一個(gè)定時(shí)器
let timeout = null
// 返回一個(gè)函數(shù),這個(gè)函數(shù)會(huì)在一個(gè)時(shí)間區(qū)間結(jié)束后調(diào)用 func
return function() {
// 如果定時(shí)器還在運(yùn)行,則跳過(guò)
if (timeout) return
// 否則,設(shè)置定時(shí)器,等待 wait 毫秒后執(zhí)行 func 函數(shù)
timeout = setTimeout(() => {
// 執(zhí)行 func 函數(shù)
func.apply(this, arguments)
// 清空定時(shí)器
timeout = null
}, wait)
}
}
在上面的代碼中,我們創(chuàng)建了一個(gè)定時(shí)器,并將它賦值給了一個(gè)變量 timeout。當(dāng)事件被觸發(fā)時(shí),我們檢查定時(shí)器是否在運(yùn)行,如果在運(yùn)行,則跳過(guò),否則執(zhí)行。
為 input 事件添加節(jié)流函數(shù):
input.addEventListener('input', throttle(handleInput, 500))
運(yùn)行效果:

實(shí)現(xiàn)防抖函數(shù)
如何判斷是否為頻繁觸發(fā)的最后一次,很簡(jiǎn)單,我們只需要在第一次觸發(fā)該事件時(shí),設(shè)定一個(gè)等待 wait 毫秒的定時(shí)器,在計(jì)時(shí)器結(jié)束之前如果再次被觸發(fā),則重新計(jì)時(shí),直至不再被觸發(fā),再去執(zhí)行傳入的函數(shù)。
根據(jù)這個(gè)思路,我們就可以很輕松的實(shí)現(xiàn)一個(gè)基礎(chǔ)的 debounce 方法。將事件處理函數(shù)設(shè)為第一個(gè)參數(shù) func,將等待時(shí)間設(shè)為第二個(gè)參數(shù) wait。
// 防抖函數(shù)
function debounce(func, wait) {
// 創(chuàng)建一個(gè)定時(shí)器
let timeout
// 返回一個(gè)函數(shù),這個(gè)函數(shù)會(huì)在一個(gè)時(shí)間區(qū)間結(jié)束后調(diào)用 func
return function() {
// 如果定時(shí)器還在運(yùn)行,則清除定時(shí)器
timeout && clearTimeout(timeout)
// 否則,設(shè)定定時(shí)器,等待 wait 毫秒后執(zhí)行 func 函數(shù)
timeout = setTimeout(() => {
// 執(zhí)行 func 函數(shù)
func.apply(this, arguments)
}, wait)
}
}
在上面的代碼中,我們創(chuàng)建了一個(gè)定時(shí)器,并將它賦值給了一個(gè)變量 timeout。當(dāng)事件被觸發(fā)時(shí),我們檢查定時(shí)器是否在運(yùn)行,如果在運(yùn)行,則清除定時(shí)器,否則執(zhí)行。
為 input 事件添加防抖函數(shù):
input.addEventListener('input', debounce(handleInput, 500))
運(yùn)行效果:

apply
上面的代碼中使用了 apply,它是 Function.prototype 的一個(gè)方法,作用是將一個(gè)函數(shù)的作用域設(shè)置為某個(gè)對(duì)象,并將參數(shù)傳入函數(shù)。
有關(guān) apply 的詳細(xì)介紹,可以點(diǎn)擊 這里 查看。
源碼下載
點(diǎn)擊 這里 查看源碼。