debounce
背景
在前端开发中会遇到一些频繁的事件触发,比如:
- button 点击事件
- input 输入事件
但有的时候,我们不想这些事件绑定的回调被频繁执行,否则可能出现表单重复提交,或者页面卡顿等情况导致降低用户体验感。为了解决这个问题,我们可以给事件的回调加上防抖 debounce
debounce
原理
经过 debounce
处理后的回调逻辑是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行回调,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行回调。
带 debounce 的 autocomplete:用户打字时一直触发输入事件,但只会在用户停止打字后一段时间后才请求数据。若每输入一个字符就请求数据,会带来很多不必要的请求,同时也增加渲染负担。(此图来自:danilorivera95.medium.com/react-autoc…
现实生活的例子就是电梯的关门时机。电梯总是在最后一个上电梯的人进入后,再等一段时间才关门。如果这段时间内,又有人进来了,那么电梯会重新开始计时,再等相同的时间才关门。
debounce 实现
基础实现
根据上述描述,我们可以实现如下代码。注意 func
的 this
绑定和入参 args
这两个小细节。
function debounce(func, wait) {
let timeout = null;
function debounced(...args) {
// 取消上次定时器回调的执行
clearTimeout(timeout);
// 重新开始计时,n 秒后执行 func
timeout = setTimeout(() => {
func.call(this, ...args);
}, wait);
}
return debounced;
}
支持立即调用
underscore 源码提供的 debounce
有第三个入参 immediate
:
immediate
为false
:在事件触发 n 秒后,才执行func
immediate
为true
:立即执行func
,等到停止触发 n 秒后,才可以重新触发执行
图中 at_begain 即 immediate。(此图来自 benalman.com/projects/jq…
function debounce(func, wait, immediate) {
let timeout = null;
function debounced(...args) {
// 保存 func 的执行结果
let res;
timeout && clearTimeout(timeout);
// 利用 timeout 是否为 null 来判断事件是否为连续事件中的第一个事件
if (!timeout && immediate) {
// 当 func 被立即执行的时候,可以让 debounced 返回 func 的执行结果
res = func.call(this, ...args);
}
timeout = setTimeout(() => {
if (!immediate) func.call(this, ...args);
timeout = null;
}, wait);
return res;
}
return debounced;
}
支持取消
underscore 中的 debounce
还支持取消。比如有一个 debounce
的时间间隔是 10 秒钟,immediate
为 true
,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行。
只需要给 debounced
挂一个 cancel
函数即可:
function debounce(func, wait, immediate) {
let timeout = null;
function debounced(...args) { ... }
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
debounce 源码
underscore 中源码的实现思路和上述代码一致,不过上面的代码中每次执行 debounced
时,总会清空旧的定时器再生成一个新的。underscore 则使用了追踪两次 debounced
调用时间间隔的思路优化了上述问题,有兴趣的读者们可以去 github 仓库中看看。