javascript防抖函數(shù)debounce詳解
定義及解讀
防抖函數(shù) debounce 指的是某個函數(shù)在某段時間內(nèi),無論觸發(fā)了多少次回調(diào),都只執(zhí)行最后一次。假如我們設(shè)置了一個等待時間 3 秒的函數(shù),在這 3 秒內(nèi)如果遇到函數(shù)調(diào)用請求就重新計(jì)時 3 秒,直至新的 3 秒內(nèi)沒有函數(shù)調(diào)用請求,此時執(zhí)行函數(shù),不然就以此類推重新計(jì)時。
舉一個小例子:假定在做公交車時,司機(jī)需等待最后一個人進(jìn)入后再關(guān)門,每次新進(jìn)一個人,司機(jī)就會把計(jì)時器清零并重新開始計(jì)時,重新等待 1 分鐘再關(guān)門,如果后續(xù) 1 分鐘內(nèi)都沒有乘客上車,司機(jī)會認(rèn)為乘客都上來了,將關(guān)門發(fā)車。
此時「上車的乘客」就是我們頻繁操作事件而不斷涌入的回調(diào)任務(wù);「1 分鐘」就是計(jì)時器,它是司機(jī)決定「關(guān)門」的依據(jù),如果有新的「乘客」上車,將清零并重新計(jì)時;「關(guān)門」就是最后需要執(zhí)行的函數(shù)。
如果你還無法理解,看下面這張圖就清晰多了,另外點(diǎn)擊 這個頁面 查看節(jié)流和防抖的可視化比較。其中 Regular 是不做任何處理的情況,throttle 是函數(shù)節(jié)流之后的結(jié)果(上一小節(jié)已介紹),debounce 是函數(shù)防抖之后的結(jié)果。
原理及實(shí)現(xiàn)
實(shí)現(xiàn)原理就是利用定時器,函數(shù)第一次執(zhí)行時設(shè)定一個定時器,之后調(diào)用時發(fā)現(xiàn)已經(jīng)設(shè)定過定時器就清空之前的定時器,并重新設(shè)定一個新的定時器,如果存在沒有被清空的定時器,當(dāng)定時器計(jì)時結(jié)束后觸發(fā)函數(shù)執(zhí)行。
實(shí)現(xiàn) 1
// 實(shí)現(xiàn) 1 // fn 是需要防抖處理的函數(shù) // wait 是時間間隔 function debounce(fn, wait = 50) { // 通過閉包緩存一個定時器 id let timer = null // 將 debounce 處理結(jié)果當(dāng)作函數(shù)返回 // 觸發(fā)事件回調(diào)時執(zhí)行這個返回函數(shù) return function(...args) { // 如果已經(jīng)設(shè)定過定時器就清空上一次的定時器 if (timer) clearTimeout(timer) // 開始設(shè)定一個新的定時器,定時器結(jié)束后執(zhí)行傳入的函數(shù) fn timer = setTimeout(() => { fn.apply(this, args) }, wait) } } // DEMO // 執(zhí)行 debounce 函數(shù)返回新函數(shù) const betterFn = debounce(() => console.log('fn 防抖執(zhí)行了'), 1000) // 停止滑動 1 秒后執(zhí)行函數(shù) () => console.log('fn 防抖執(zhí)行了') document.addEventListener('scroll', betterFn)
實(shí)現(xiàn) 2
上述實(shí)現(xiàn)方案已經(jīng)可以解決大部分使用場景了,不過想要實(shí)現(xiàn)第一次觸發(fā)回調(diào)事件就執(zhí)行 fn 有點(diǎn)力不從心了,這時候我們來改寫下 debounce 函數(shù),加上第一次觸發(fā)立即執(zhí)行的功能。
// 實(shí)現(xiàn) 2 // immediate 表示第一次是否立即執(zhí)行 function debounce(fn, wait = 50, immediate) { let timer = null return function(...args) { if (timer) clearTimeout(timer) // ------ 新增部分 start ------ // immediate 為 true 表示第一次觸發(fā)后執(zhí)行 // timer 為空表示首次觸發(fā) if (immediate && !timer) { fn.apply(this, args) } // ------ 新增部分 end ------ timer = setTimeout(() => { fn.apply(this, args) }, wait) } } // DEMO
// 執(zhí)行 debounce 函數(shù)返回新函數(shù)
const betterFn = debounce(() => console.log('fn 防抖執(zhí)行了'), 1000, true)
// 第一次觸發(fā) scroll 執(zhí)行一次 fn,后續(xù)只有在停止滑動 1 秒后才執(zhí)行函數(shù) fn
document.addEventListener('scroll', betterFn)
實(shí)現(xiàn)原理比較簡單,判斷傳入的 immediate 是否為 true,另外需要額外判斷是否是第一次執(zhí)行防抖函數(shù),判斷依舊就是 timer 是否為空,所以只要 immediate && !timer 返回 true 就執(zhí)行 fn 函數(shù),即 fn.apply(this, args)。
加強(qiáng)版 throttle
現(xiàn)在考慮一種情況,如果用戶的操作非常頻繁,不等設(shè)置的延遲時間結(jié)束就進(jìn)行下次操作,會頻繁的清除計(jì)時器并重新生成,所以函數(shù) fn 一直都沒辦法執(zhí)行,導(dǎo)致用戶操作遲遲得不到響應(yīng)。
有一種思想是將「節(jié)流」和「防抖」合二為一,變成加強(qiáng)版的節(jié)流函數(shù),關(guān)鍵點(diǎn)在于「 wait 時間內(nèi),可以重新生成定時器,但只要 wait 的時間到了,必須給用戶一個響應(yīng)」。這種合體思路恰好可以解決上面提出的問題。
給出合二為一的代碼之前先來回顧下 throttle 函數(shù),上一小節(jié)中有詳細(xì)的介紹。
// fn 是需要執(zhí)行的函數(shù) // wait 是時間間隔 const throttle = (fn, wait = 50) => { // 上一次執(zhí)行 fn 的時間 let previous = 0 // 將 throttle 處理結(jié)果當(dāng)作函數(shù)返回 return function(...args) { // 獲取當(dāng)前時間,轉(zhuǎn)換成時間戳,單位毫秒 let now = +new Date() // 將當(dāng)前時間和上一次執(zhí)行函數(shù)的時間進(jìn)行對比 // 大于等待時間就把 previous 設(shè)置為當(dāng)前時間并執(zhí)行函數(shù) fn if (now - previous > wait) { previous = now fn.apply(this, args) } } }
結(jié)合 throttle 和 debounce 代碼,加強(qiáng)版節(jié)流函數(shù) throttle 如下,新增邏輯在于當(dāng)前觸發(fā)時間和上次觸發(fā)的時間差小于時間間隔時,設(shè)立一個新的定時器,相當(dāng)于把 debounce 代碼放在了小于時間間隔部分。
// fn 是需要節(jié)流處理的函數(shù) // wait 是時間間隔 function throttle(fn, wait) { // previous 是上一次執(zhí)行 fn 的時間 // timer 是定時器 let previous = 0, timer = null // 將 throttle 處理結(jié)果當(dāng)作函數(shù)返回 return function (...args) { // 獲取當(dāng)前時間,轉(zhuǎn)換成時間戳,單位毫秒 let now = +new Date() // ------ 新增部分 start ------ // 判斷上次觸發(fā)的時間和本次觸發(fā)的時間差是否小于時間間隔 if (now - previous < wait) { // 如果小于,則為本次觸發(fā)操作設(shè)立一個新的定時器 // 定時器時間結(jié)束后執(zhí)行函數(shù) fn if (timer) clearTimeout(timer) timer = setTimeout(() => { previous = now fn.apply(this, args) }, wait) // ------ 新增部分 end ------ } else { // 第一次執(zhí)行 // 或者時間間隔超出了設(shè)定的時間間隔,執(zhí)行函數(shù) fn previous = now fn.apply(this, args) } } } // DEMO // 執(zhí)行 throttle 函數(shù)返回新函數(shù) const betterFn = throttle(() => console.log('fn 節(jié)流執(zhí)行了'), 1000) // 第一次觸發(fā) scroll 執(zhí)行一次 fn,每隔 1 秒后執(zhí)行一次函數(shù) fn,停止滑動 1 秒后再執(zhí)行函數(shù) fn document.addEventListener('scroll', betterFn)
看完整段代碼會發(fā)現(xiàn)這個思想和上篇文章介紹的 underscore 中 throttle 的實(shí)現(xiàn)思想非常相似。
underscore 源碼解析
看完了上文的基本版代碼,感覺還是比較輕松的,現(xiàn)在來學(xué)習(xí)下 underscore 是如何實(shí)現(xiàn) debounce 函數(shù)的,學(xué)習(xí)一下優(yōu)秀的思想,直接上代碼和注釋,本源碼解析依賴于 underscore 1.9.1 版本實(shí)現(xiàn)。
// 此處的三個參數(shù)上文都有解釋 _.debounce = function(func, wait, immediate) { // timeout 表示定時器 // result 表示 func 執(zhí)行返回值 var timeout, result; // 定時器計(jì)時結(jié)束后 // 1、清空計(jì)時器,使之不影響下次連續(xù)事件的觸發(fā) // 2、觸發(fā)執(zhí)行 func var later = function(context, args) { timeout = null; // if (args) 判斷是為了過濾立即觸發(fā)的 // 關(guān)聯(lián)在于 _.delay 和 restArguments if (args) result = func.apply(context, args); }; // 將 debounce 處理結(jié)果當(dāng)作函數(shù)返回 var debounced = restArguments(function(args) { if (timeout) clearTimeout(timeout); if (immediate) { // 第一次觸發(fā)后會設(shè)置 timeout, // 根據(jù) timeout 是否為空可以判斷是否是首次觸發(fā) var callNow = !timeout; timeout = setTimeout(later, wait); if (callNow) result = func.apply(this, args); } else { // 設(shè)置定時器 timeout = _.delay(later, wait, this, args); } return result; }); // 新增 手動取消 debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; }; // 根據(jù)給定的毫秒 wait 延遲執(zhí)行函數(shù) func _.delay = restArguments(function(func, wait, args) { return setTimeout(function() { return func.apply(null, args); }, wait); });
相比上文的基本版實(shí)現(xiàn),underscore 多了以下幾點(diǎn)功能。
1、函數(shù) func 的執(zhí)行結(jié)束后返回結(jié)果值 result
2、定時器計(jì)時結(jié)束后清除 timeout,使之不影響下次連續(xù)事件的觸發(fā)
3、新增了手動取消功能 cancel
4、immediate 為 true 后只會在第一次觸發(fā)時執(zhí)行,頻繁觸發(fā)回調(diào)結(jié)束后不會再執(zhí)行
小結(jié)
- 函數(shù)節(jié)流和防抖都是「閉包」、「高階函數(shù)」的應(yīng)用
- 函數(shù)節(jié)流 throttle 指的是某個函數(shù)在一定時間間隔內(nèi)(例如 3 秒)執(zhí)行一次,在這 3 秒內(nèi) 無視后來產(chǎn)生的函數(shù)調(diào)用請求
- 節(jié)流可以理解為養(yǎng)金魚時擰緊水龍頭放水,3 秒一滴
- 「管道中的水」就是我們頻繁操作事件而不斷涌入的回調(diào)任務(wù),它需要接受「水龍頭」安排
- 「水龍頭」就是節(jié)流閥,控制水的流速,過濾無效的回調(diào)任務(wù)
- 「滴水」就是每隔一段時間執(zhí)行一次函數(shù)
- 「3 秒」就是間隔時間,它是「水龍頭」決定「滴水」的依據(jù)
- 應(yīng)用:監(jiān)聽滾動事件添加節(jié)流函數(shù)后,每隔固定的一段時間執(zhí)行一次
- 實(shí)現(xiàn)方案 1:用時間戳來判斷是否已到執(zhí)行時間,記錄上次執(zhí)行的時間戳,然后每次觸發(fā)后執(zhí)行回調(diào),判斷當(dāng)前時間距離上次執(zhí)行時間的間隔是否已經(jīng)達(dá)到時間差(Xms) ,如果是則執(zhí)行,并更新上次執(zhí)行的時間戳,如此循環(huán)
- 實(shí)現(xiàn)方案 2:使用定時器,比如當(dāng) scroll 事件剛觸發(fā)時,打印一個 hello world,然后設(shè)置個 1000ms 的定時器,此后每次觸發(fā) scroll 事件觸發(fā)回調(diào),如果已經(jīng)存在定時器,則回調(diào)不執(zhí)行方法,直到定時器觸發(fā),handler 被清除,然后重新設(shè)置定時器
- 節(jié)流可以理解為養(yǎng)金魚時擰緊水龍頭放水,3 秒一滴
- 函數(shù)防抖 debounce 指的是某個函數(shù)在某段時間內(nèi),無論觸發(fā)了多少次回調(diào),都只執(zhí)行最后一次
- 防抖可以理解為司機(jī)等待最后一個人進(jìn)入后再關(guān)門,每次新進(jìn)一個人,司機(jī)就會把計(jì)時器清零并重新開始計(jì)時
- 「上車的乘客」就是我們頻繁操作事件而不斷涌入的回調(diào)任務(wù)
- 「1 分鐘」就是計(jì)時器,它是司機(jī)決定「關(guān)門」的依據(jù),如果有新的「乘客」上車,將清零并重新計(jì)時
- 「關(guān)門」就是最后需要執(zhí)行的函數(shù)
- 應(yīng)用:input 輸入回調(diào)事件添加防抖函數(shù)后,只會在停止輸入后觸發(fā)一次
- 實(shí)現(xiàn)方案:使用定時器,函數(shù)第一次執(zhí)行時設(shè)定一個定時器,之后調(diào)用時發(fā)現(xiàn)已經(jīng)設(shè)定過定時器就清空之前的定時器,并重新設(shè)定一個新的定時器,如果存在沒有被清空的定時器,當(dāng)定時器計(jì)時結(jié)束后觸發(fā)函數(shù)執(zhí)行
- 防抖可以理解為司機(jī)等待最后一個人進(jìn)入后再關(guān)門,每次新進(jìn)一個人,司機(jī)就會把計(jì)時器清零并重新開始計(jì)時
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JS用斜率判斷鼠標(biāo)進(jìn)入DIV四個方向的方法
在網(wǎng)上去搜判斷鼠標(biāo)移入div移入移出的方法大同小異,下面小編給大家分享一篇文章關(guān)于js判斷鼠標(biāo)進(jìn)入div方向的代碼,感興趣的朋友一起看看吧2016-11-11JavaScript+html5 canvas制作的百花齊放效果完整實(shí)例
這篇文章主要介紹了JavaScript+html5 canvas制作的百花齊放效果,結(jié)合完整實(shí)例形式分析了使用html5的canvas技術(shù)動態(tài)繪制圖形的技巧,需要的朋友可以參考下2016-01-01在js文件中引入(調(diào)用)另一個js文件的三種方法
這篇文章主要介紹了在js文件中引入(調(diào)用)另一個js文件的三種方法,幫助大家更好的理解和學(xué)習(xí)JavaScript,感興趣的朋友可以了解下2020-09-09JavaScript防抖與節(jié)流的實(shí)現(xiàn)與注意事項(xiàng)
防抖和節(jié)流嚴(yán)格算起來應(yīng)該屬于性能優(yōu)化的知識,但實(shí)際上遇到的頻率相當(dāng)高,處理不當(dāng)或者放任不管就容易引起瀏覽器卡死,下面這篇文章主要給大家介紹了關(guān)于JavaScript防抖與節(jié)流的實(shí)現(xiàn)與注意事項(xiàng),需要的朋友可以參考下2022-03-03微信小程序開發(fā)搜索功能實(shí)現(xiàn)(前端+后端+數(shù)據(jù)庫)
這篇文章主要介紹了微信小程序開發(fā)搜索功能實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03