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