淺析函數(shù)防抖與函數(shù)節(jié)流

前言

最近和前端的小伙伴們,在討論面試題的時候。談到了函數(shù)防抖和函數(shù)節(jié)流的應(yīng)用場景和原理。于是,想深入研究一下兩者的異同。對于后端而言,函數(shù)防抖、函數(shù)節(jié)流的使用場景并不是很多。但是,對于前端使用卻是很常見。常見實用場景,有滾動加載、搜索框輸入、窗口大小拖拽 Resize。

概念

函數(shù)防抖(debounce)

函數(shù)防抖,就是指觸發(fā)事件后在 n 秒內(nèi)函數(shù)只能執(zhí)行一次,如果在 n 秒內(nèi)又觸發(fā)了事件,則會重新計算函數(shù)執(zhí)行時間。

簡單的說,當(dāng)一個動作連續(xù)觸發(fā),則只執(zhí)行最后一次。

打個比方,坐公交,司機(jī)需要等最后一個人進(jìn)入才能關(guān)門。每次進(jìn)入一個人,司機(jī)就會多等待幾秒再關(guān)門。

函數(shù)節(jié)流(throttle)

限制一個函數(shù)在一定時間內(nèi)只能執(zhí)行一次。

舉個例子,乘坐地鐵,過閘機(jī)時,每個人進(jìn)入后3秒后門關(guān)閉,等待下一個人進(jìn)入。

為了方便理解,我們首先通過一個可視化的工具,感受一下三種環(huán)境(正常情況、函數(shù)防抖情況 debounce、函數(shù)節(jié)流 throttle)下,對于mousemove事件回調(diào)的執(zhí)行情況。

三種環(huán)境下,mousemove事件執(zhí)行分布圖

豎線的疏密代表事件執(zhí)行的頻繁程度??梢钥吹剑G闆r下,豎線非常密集,函數(shù)執(zhí)行的很頻繁。而debounce(函數(shù)防抖)則很稀疏,只有當(dāng)鼠標(biāo)停止移動時才會執(zhí)行一次。throttle(函數(shù)節(jié)流)分布的較為均已,每過一段時間就會執(zhí)行一次。

常見應(yīng)用場景

函數(shù)防抖的應(yīng)用場景

連續(xù)的事件,只需觸發(fā)一次回調(diào)的場景有:

  • 搜索框搜索輸入。只需用戶最后一次輸入完,再發(fā)送請求
  • 手機(jī)號、郵箱驗證輸入檢測
  • 窗口大小Resize。只需窗口調(diào)整完成后,計算窗口大小。防止重復(fù)渲染。

函數(shù)節(jié)流的應(yīng)用場景

間隔一段時間執(zhí)行一次回調(diào)的場景有:

  • 滾動加載,加載更多或滾到底部監(jiān)聽
  • 谷歌搜索框,搜索聯(lián)想功能
  • 高頻點擊提交,表單重復(fù)提交

實現(xiàn)原理

函數(shù)防抖(debounce)

函數(shù)防抖的簡單實現(xiàn):

const _.debounce = (func, wait) => {
  let timer;

  return () => {
    clearTimeout(timer);
    timer = setTimeout(func, wait);
  };
};

函數(shù)防抖在執(zhí)行目標(biāo)方法時,會等待一段時間。當(dāng)又執(zhí)行相同方法時,若前一個定時任務(wù)未執(zhí)行完,則 clear 掉定時任務(wù),重新定時。

函數(shù)節(jié)流(throttle)

1)函數(shù)節(jié)流的 setTimeout 版簡單實現(xiàn)

const _.throttle = (func, wait) => {
  let timer;

  return () => {
    if (timer) {
      return;
    }

    timer = setTimeout(() => {
      func();
      timer = null;
    }, wait);
  };
};

函數(shù)節(jié)流的目的,是為了限制函數(shù)一段時間內(nèi)只能執(zhí)行一次。因此,通過使用定時任務(wù),延時方法執(zhí)行。在延時的時間內(nèi),方法若被觸發(fā),則直接退出方法。從而,實現(xiàn)函數(shù)一段時間內(nèi)只執(zhí)行一次。

2)函數(shù)節(jié)流的時間戳版簡單實現(xiàn)
根據(jù)函數(shù)節(jié)流的原理,我們也可以不依賴 setTimeout實現(xiàn)函數(shù)節(jié)流。

const throttle = (func, wait) => {
  let last = 0;
  return () => {
    const current_time = +new Date();
    if (current_time - last > wait) {
      func.apply(this, arguments);
      last = +new Date();
    }
  };
};

其實現(xiàn)原理,通過比對上一次執(zhí)行時間與本次執(zhí)行時間的時間差與間隔時間的大小關(guān)系,來判斷是否執(zhí)行函數(shù)。若時間差大于間隔時間,則立刻執(zhí)行一次函數(shù)。并更新上一次執(zhí)行時間。

異同比較

相同點:

  • 都可以通過使用 setTimeout 實現(xiàn)。
  • 目的都是,降低回調(diào)執(zhí)行頻率。節(jié)省計算資源。

不同點:

  • 函數(shù)防抖,在一段連續(xù)操作結(jié)束后,處理回調(diào),利用 clearTimeout 和 setTimeout 實現(xiàn)。函數(shù)節(jié)流,在一段連續(xù)操作中,每一段時間只執(zhí)行一次,頻率較高的事件中使用來提高性能。
  • 函數(shù)防抖關(guān)注一定時間連續(xù)觸發(fā),只在最后執(zhí)行一次,而函數(shù)節(jié)流側(cè)重于一段時間內(nèi)只執(zhí)行一次。

lodash中的 Debounce 、Throttle

最后討論一下 lodash中 debounce的使用和源碼淺析。之所以分析 debounce,是因為在lodash中,throttle 是基于 debounce 實現(xiàn)的。如果能理解了 debounce的實現(xiàn),也就能快速掌握 throttle。

如何使用 debounce

首先,看一下 debounce 的API。需要注意的是,API中的第三個參數(shù) options。一共有3個屬性,分別是 leading、maxWaittrailing。含義分別是在開始之前調(diào)用、最大等待時間、在延遲后調(diào)用。

leadingtrailing的區(qū)別,一個是在等待前被調(diào)用,一個是等待后被調(diào)用。我們上文中,提到的 debounce 的簡單實現(xiàn),都是等待后被調(diào)用。lodash 中默認(rèn)(trailing: true)的也為等待后被調(diào)用。

/**
 * 創(chuàng)建一個會在 `wait` 毫秒后調(diào)用 `func` 的防抖動函數(shù)。
 * 最后一次傳入 `func` 參數(shù)會傳給防抖動函數(shù),隨后調(diào)用的防抖動函數(shù)返回是最后一次 func 調(diào)用的結(jié)果。
 * 防抖動函數(shù)提供 cancel 方法來取消延遲的函數(shù)調(diào)用 以及 flush 方法來立即執(zhí)行函數(shù)調(diào)用。
 *
 * 注意: 如果 leading 和 trailing 都設(shè)定為 true,則 func 允許 trailing 方式調(diào)用的條件為: 在 wait 期間多次調(diào)用。
 * 
 * @param {Function} func 要防抖動的函數(shù)
 * @param {number} [wait=0] 需要延遲的毫秒數(shù)
 * @param {Object} [options={}] 選項對象
 * @param {boolean} [options.leading=false] 指定調(diào)用在延遲開始前
 * @param {number} [options.maxWait] 設(shè)置 `func` 允許被延遲的最大值
 * @param {boolean} [options.trailing=true] 指定調(diào)用在延遲結(jié)束后
 * @returns {Function} 返回一個具有防抖動功能的函數(shù)
 */

_.debounce(func, [wait=0], [options])

使用示例

// 正確的用法
$(window).on('scroll', _.debounce(doSomething, 200));

// 錯誤的用法
// 會導(dǎo)致多次調(diào)用debounce
$(window).on('scroll', function() {
   _.debounce(doSomething, 300); 
});

// 點擊后立即執(zhí)行 sendMail
$('.btn').on('click', _.debounce(sendMail, 300, {
  'leading': true,
  'trailing': false
}));

// `batchLog` 調(diào)用1次之后,1秒內(nèi)會被觸發(fā)。
const debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });

實現(xiàn)

推薦先粗略閱讀 lodash源碼,若難度較大,可以參考這篇博文——聊聊lodash的debounce實現(xiàn),以及作者的 debounce 簡單實現(xiàn)
54行實現(xiàn) debouncethrottle,雖然功能不如 lodash 強(qiáng)大,但是非常適合理解 debounce的實現(xiàn)。

在理解 debounce 實現(xiàn)原理上(若不理解,可以返回閱讀上文中——函數(shù)防抖的簡單實現(xiàn)),主要從三個功能點理解:

  • leading 功能的實現(xiàn)
  • maxWait 功能的實現(xiàn)
  • trailing 控制

總結(jié)

最后,總結(jié)一下函數(shù)防抖與函數(shù)節(jié)流的區(qū)別。函數(shù)防抖,將多次執(zhí)行的事件合并成一次。函數(shù)節(jié)流,保持一段時間執(zhí)行一次。推薦閱讀「涂鴉碼龍」翻譯的這篇 - 實例解析防抖動(Debouncing)和節(jié)流閥(Throttling),加深理解。文章豐富的實例,可深刻感受一下兩者的區(qū)別。

在不是很理解 debounce的API的情況下,直接閱讀lodash源碼,花了2個晚上看得懂云里霧里。后面,重新閱讀API文檔,弄明白了 leadingtrailing的目的。很快就看懂了 debounce的源碼。因此,建議閱讀源碼前,先理解API中各個參數(shù)的用處。帶著目的看源碼會容易一些。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容