前言
最近和前端的小伙伴們,在討論面試題的時候。談到了函數(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í)行情況。

豎線的疏密代表事件執(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、maxWait、trailing。含義分別是在開始之前調(diào)用、最大等待時間、在延遲后調(diào)用。
leading與 trailing的區(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) debounce 和 throttle,雖然功能不如 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文檔,弄明白了 leading 和 trailing的目的。很快就看懂了 debounce的源碼。因此,建議閱讀源碼前,先理解API中各個參數(shù)的用處。帶著目的看源碼會容易一些。
參考資料
- 函數(shù)節(jié)流與函數(shù)防抖
- SF - 函數(shù)節(jié)流與函數(shù)防抖
- JavaScript 函數(shù)節(jié)流和函數(shù)去抖應(yīng)用場景辨析
- 淺談 Underscore.js 中 _.throttle 和 _.debounce 的差異
- 可視化展示 debounce & throttle
- Debouncing and Throttling Explained Through Examples
- source-code-learning
- 實例解析防抖動(Debouncing)和節(jié)流閥(Throttling)
- lodash - debounce 源碼