高頻:手寫一個防抖函數 Debounce
本文轉載自微信公眾號「三分鐘學前端」,作者sisterAn 。轉載本文請聯系三分鐘學前端公眾號。
我們上次 手寫了一個節流函數throttle ,這周我們繼續手寫手寫一個防抖函數 debounce
手寫一個 debounce
防抖函數 debounce 指的是某個函數在某段時間內,無論觸發了多少次回調,都只執行最后一次。
實現原理就是利用定時器,函數第一次執行時設定一個定時器,之后調用時發現已經設定過定時器就清空之前的定時器,并重新設定一個新的定時器,如果存在沒有被清空的定時器,當定時器計時結束后觸發函數執行。
- // fn 是需要防抖處理的函數
- // wait 是時間間隔
- function debounce(fn, wait = 50) {
- // 通過閉包緩存一個定時器 id
- let timer = null
- // 將 debounce 處理結果當作函數返回
- // 觸發事件回調時執行這個返回函數
- return function(...args) {
- // this保存給context
- const context = this
- // 如果已經設定過定時器就清空上一次的定時器
- if (timer) clearTimeout(timer)
- // 開始設定一個新的定時器,定時器結束后執行傳入的函數 fn
- timer = setTimeout(() => {
- fn.apply(context, args)
- }, wait)
- }
- }
- // DEMO
- // 執行 debounce 函數返回新函數
- const betterFn = debounce(() => console.log('fn 防抖執行了'), 1000)
- // 停止滑動 1 秒后執行函數 () => console.log('fn 防抖執行了')
- document.addEventListener('scroll', betterFn)
不過 underscore 中的 debounce 還有第三個參數:immediate 。這個參數是做什么用的呢?
傳參 immediate 為 true, debounce會在 wait 時間間隔的開始調用這個函數 。(注:并且在 wait 的時間之內,不會再次調用。)在類似不小心點了提交按鈕兩下而提交了兩次的情況下很有用。
把 true 傳遞給 immediate 參數,會讓 debounce 在 wait 時間開始計算之前就觸發函數(也就是沒有任何延時就觸發函數),而不是過了 wait 時間才觸發函數,而且在 wait 時間內也不會觸發(相當于把 fn 的執行鎖住)。如果不小心點了兩次提交按鈕,第二次提交就會不會執行。
那我們根據 immediate 的值來決定如何執行 fn 。如果是 immediate 的情況下,我們立即執行 fn ,并在 wait 時間內鎖住 fn 的執行, wait 時間之后再觸發,才會重新執行 fn ,以此類推。
- // immediate 表示第一次是否立即執行
- function debounce(fn, wait = 50, immediate) {
- let timer = null
- return function(...args) {
- // this保存給context
- const context = this
- if (timer) clearTimeout(timer)
- // immediate 為 true 表示第一次觸發后執行
- // timer 為空表示首次觸發
- if (immediate && !timer) {
- fn.apply(context, args)
- }
- timer = setTimeout(() => {
- fn.apply(context, args)
- }, wait)
- }
- }
- // DEMO
- // 執行 debounce 函數返回新函數
- const betterFn = debounce(() => console.log('fn 防抖執行了'), 1000, true)
- // 第一次觸發 scroll 執行一次 fn,后續只有在停止滑動 1 秒后才執行函數 fn
- document.addEventListener('scroll', betterFn)
underscore 源碼解析
看完了上文的基本版代碼,感覺還是比較輕松的,現在來學習下 underscore 是如何實現 debounce 函數的,學習一下優秀的思想,直接上代碼和注釋,本源碼解析依賴于 underscore 1.9.1 版本實現。
- // 此處的三個參數上文都有解釋
- _.debounce = function(func, wait, immediate) {
- // timeout 表示定時器
- // result 表示 func 執行返回值
- var timeout, result;
- // 定時器計時結束后
- // 1、清空計時器,使之不影響下次連續事件的觸發
- // 2、觸發執行 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 多了以下幾點功能。
1、函數 func 的執行結束后返回結果值 result
2、定時器計時結束后清除 timeout ,使之不影響下次連續事件的觸發
3、新增了手動取消功能 cancel
4、immediate 為 true 后只會在第一次觸發時執行,頻繁觸發回調結束后不會再執行
參考
深入淺出防抖函數 debounce
前端面試題——自己實現debounce






























