更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端
------ 如下是正文 ------git
上一節咱們認識了節流函數 throttle,瞭解了它的定義、實現原理以及在 underscore 中的實現。這一小節會繼續以前的篇幅聊聊防抖函數 debounce,結構是同樣的,將分別介紹定義、實現原理並給出了 2 種實現代碼並在最後介紹在 underscore 中的實現,歡迎你們拍磚。github
有什麼想法或者意見均可以在評論區留言,下圖是本文的思惟導圖,高清思惟導圖和更多文章請看個人 Github。面試
防抖函數 debounce 指的是某個函數在某段時間內,不管觸發了多少次回調,都只執行最後一次。假如咱們設置了一個等待時間 3 秒的函數,在這 3 秒內若是遇到函數調用請求就從新計時 3 秒,直至新的 3 秒內沒有函數調用請求,此時執行函數,否則就以此類推從新計時。緩存
舉一個小例子:假定在作公交車時,司機需等待最後一我的進入後再關門,每次新進一我的,司機就會把計時器清零並從新開始計時,從新等待 1 分鐘再關門,若是後續 1 分鐘內都沒有乘客上車,司機會認爲乘客都上來了,將關門發車。性能優化
此時「上車的乘客」就是咱們頻繁操做事件而不斷涌入的回調任務;「1 分鐘」就是計時器,它是司機決定「關門」的依據,若是有新的「乘客」上車,將清零並從新計時;「關門」就是最後須要執行的函數。閉包
若是你還沒法理解,看下面這張圖就清晰多了,另外點擊 這個頁面 查看節流和防抖的可視化比較。其中 Regular 是不作任何處理的狀況,throttle 是函數節流以後的結果(上一小節已介紹),debounce 是函數防抖以後的結果。app
實現原理就是利用定時器,函數第一次執行時設定一個定時器,以後調用時發現已經設定過定時器就清空以前的定時器,並從新設定一個新的定時器,若是存在沒有被清空的定時器,當定時器計時結束後觸發函數執行。前端性能
// 實現 1
// fn 是須要防抖處理的函數
// wait 是時間間隔
function debounce(fn, wait = 50) {
// 經過閉包緩存一個定時器 id
let timer = null
// 將 debounce 處理結果看成函數返回
// 觸發事件回調時執行這個返回函數
return function(...args) {
// 若是已經設定過定時器就清空上一次的定時器
if (timer) clearTimeout(timer)
// 開始設定一個新的定時器,定時器結束後執行傳入的函數 fn
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
// DEMO
// 執行 debounce 函數返回新函數
const betterFn = debounce(() => console.log('fn 防抖執行了'), 1000)
// 中止滑動 1 秒後執行函數 () => console.log('fn 防抖執行了')
document.addEventListener('scroll', betterFn)
複製代碼
上述實現方案已經能夠解決大部分使用場景了,不過想要實現第一次觸發回調事件就執行 fn 有點力不從心了,這時候咱們來改寫下 debounce 函數,加上第一次觸發當即執行的功能。函數
// 實現 2
// immediate 表示第一次是否當即執行
function debounce(fn, wait = 50, immediate) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
// ------ 新增部分 start ------
// immediate 爲 true 表示第一次觸發後執行
// timer 爲空表示首次觸發
if (immediate && !timer) {
fn.apply(this, args)
}
// ------ 新增部分 end ------
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
// DEMO
// 執行 debounce 函數返回新函數
const betterFn = debounce(() => console.log('fn 防抖執行了'), 1000, true)
// 第一次觸發 scroll 執行一次 fn,後續只有在中止滑動 1 秒後才執行函數 fn
document.addEventListener('scroll', betterFn)
複製代碼
實現原理比較簡單,判斷傳入的 immediate 是否爲 true,另外須要額外判斷是不是第一次執行防抖函數,判斷依舊就是 timer 是否爲空,因此只要 immediate && !timer
返回 true 就執行 fn 函數,即 fn.apply(this, args)
。
如今考慮一種狀況,若是用戶的操做很是頻繁,不等設置的延遲時間結束就進行下次操做,會頻繁的清除計時器並從新生成,因此函數 fn 一直都沒辦法執行,致使用戶操做遲遲得不到響應。
有一種思想是將「節流」和「防抖」合二爲一,變成增強版的節流函數,關鍵點在於「 wait 時間內,能夠從新生成定時器,但只要 wait 的時間到了,必須給用戶一個響應」。這種合體思路剛好能夠解決上面提出的問題。
給出合二爲一的代碼以前先來回顧下 throttle 函數,上一小節中有詳細的介紹。
// fn 是須要執行的函數
// wait 是時間間隔
const throttle = (fn, wait = 50) => {
// 上一次執行 fn 的時間
let previous = 0
// 將 throttle 處理結果看成函數返回
return function(...args) {
// 獲取當前時間,轉換成時間戳,單位毫秒
let now = +new Date()
// 將當前時間和上一次執行函數的時間進行對比
// 大於等待時間就把 previous 設置爲當前時間並執行函數 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}
複製代碼
結合 throttle 和 debounce 代碼,增強版節流函數 throttle 以下,新增邏輯在於當前觸發時間和上次觸發的時間差小於時間間隔時,設立一個新的定時器,至關於把 debounce 代碼放在了小於時間間隔部分。
// fn 是須要節流處理的函數
// wait 是時間間隔
function throttle(fn, wait) {
// previous 是上一次執行 fn 的時間
// timer 是定時器
let previous = 0, timer = null
// 將 throttle 處理結果看成函數返回
return function (...args) {
// 獲取當前時間,轉換成時間戳,單位毫秒
let now = +new Date()
// ------ 新增部分 start ------
// 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔
if (now - previous < wait) {
// 若是小於,則爲本次觸發操做設立一個新的定時器
// 定時器時間結束後執行函數 fn
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
previous = now
fn.apply(this, args)
}, wait)
// ------ 新增部分 end ------
} else {
// 第一次執行
// 或者時間間隔超出了設定的時間間隔,執行函數 fn
previous = now
fn.apply(this, args)
}
}
}
// DEMO
// 執行 throttle 函數返回新函數
const betterFn = throttle(() => console.log('fn 節流執行了'), 1000)
// 第一次觸發 scroll 執行一次 fn,每隔 1 秒後執行一次函數 fn,中止滑動 1 秒後再執行函數 fn
document.addEventListener('scroll', betterFn)
複製代碼
看完整段代碼會發現這個思想和上篇文章介紹的 underscore 中 throttle 的實現思想很是類似。
看完了上文的基本版代碼,感受仍是比較輕鬆的,如今來學習下 underscore 是如何實現 debounce 函數的,學習一下優秀的思想,直接上代碼和註釋,本源碼解析依賴於 underscore 1.9.1 版本實現。
// 此處的三個參數上文都有解釋
_.debounce = function(func, wait, immediate) {
// timeout 表示定時器
// result 表示 func 執行返回值
var timeout, result;
// 定時器計時結束後
// 一、清空計時器,使之不影響下次連續事件的觸發
// 二、觸發執行 func
var later = function(context, args) {
timeout = null;
// if (args) 判斷是爲了過濾當即觸發的
// 關聯在於 _.delay 和 restArguments
if (args) result = func.apply(context, args);
};
// 將 debounce 處理結果看成函數返回
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
// 第一次觸發後會設置 timeout,
// 根據 timeout 是否爲空能夠判斷是不是首次觸發
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;
};
// 根據給定的毫秒 wait 延遲執行函數 func
_.delay = restArguments(function(func, wait, args) {
return setTimeout(function() {
return func.apply(null, args);
}, wait);
});
複製代碼
相比上文的基本版實現,underscore 多瞭如下幾點功能。
函數節流和防抖都是「閉包」、「高階函數」的應用
函數節流 throttle 指的是某個函數在必定時間間隔內(例如 3 秒)執行一次,在這 3 秒內 無視後來產生的函數調用請求
函數防抖 debounce 指的是某個函數在某段時間內,不管觸發了多少次回調,都只執行最後一次
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙: