摘要
防抖和节流是开发过程中经常会遇到的业务问题,本文旨在剖析其实现逻辑,帮助更多开发者掌握并运用此项开发技巧,提高业务水平
防抖:无论事件处理函数触发多少次,其中的每一次执行都将延迟 N秒执行
节流:事件处理函数无论触发多少次在N秒内只执行一次
防抖函数具体实现
根据防抖函数的定义,首先我们需要一个提供事件处理的函数,这里用 fn举例
const button = document.getElementsByTagName('button').item(0);
button.addEventListener('click', fn); // 使用 click 事件模拟更加直观
function fn() {
console.log(new Date().getSeconds()); // 这里 fn 每次执行时会输出当前秒数,用于展示延迟时间
}
由于事件处理需要一个函数,且我们需要对已有的函数进行改造,但为了尽量不修改原函数。因此可以想到使用函数闭包来解决这个问题
function debounce(event, wait) {
return function (...args) { // 当存在后续参数时给予接收
};
}
然后,因为需要延迟执行,所以可以使用定时器实现,且因不需要循环执行,则选择采用 setTimeout
function debounce(event, wait) { // 接收处理函数和定义的延迟时间
return function (...args) { // 当存在后续参数时给予接收
timer = setTimeout(() => {
event.apply(this, args); // 采用 apply 是因为 args 的类型为数组
}, wait);
};
}
此时,一个粗糙的防抖函数就实现了,但此时,其功能还不稳定。由于每一次触发完毕都会设定一个定时器,因此需要对其进行清除,以免干扰下一次延迟
function debounce(event, wait) { // 接收处理函数和定义的延迟时间
let timer = null; // 由于后续需要清除定时器,因此需先声明变量作为定时器的标识
return function (...args) { // 当存在后续参数时给予接收
if (timer) clearTimeout(timer); // 触发后清除上一次创建的定时器
// 延时执行给定的处理函数
timer = setTimeout(() => {
event.apply(this, args); // 采用 apply 是因为 args 的类型为数组
}, wait);
};
}
到此,一个功能正常的防抖函数就实现了。此时每一次触发都需延迟 wait 毫秒,而实际业务中首次触发并不需要延时,否则用户往往会感到莫名其妙。因此,我们选择加入第三个参数 immediate
function debounce(event, wait, immediate) {
let timer = null;
return function (...args) {
clearTimeout(timer);
if (!timer && immediate) event.apply(this, args); // 若计时器为空且立即参数为真即执行函数
// 若不满足条件则按正常计划使用定时器延迟执行
else {
timer = setTimeout(() => {
event.apply(this, args);
}, wait);
}
}
}
至此,一个正常的具有使用价值的防抖函数就完成了。其使用场景在于需要延迟执行请求或操作的情况下,如窗口变化或表单延迟校验等
节流函数具体实现
根据节流函数的定义,当处理函数多次触发时,只在规定时间内执行一次。这里提供了两种思路:时间戳和定时器
时间戳的原理是当第一次执行前保存触发时间,后续触发时判断是否大于或等于必须等待的时间,从而达到限制执行次数的目的
定时器则和防抖函数延时处理的逻辑差不太多,当重复触发时清除定时器,并执行最后一次触发的定时器,以达到限制执行次数的目的
时间戳方式
此种方式的缺点是最后一次触发事件时不会执行。经查阅网络资料,发现大多是把保存上次触发时间的变量放进了函数内,导致每次执行函数时old变量都会被重置,以致丢失保存的时间,无法实现节流效果。当然,也有可能是我未发现其中深意,如有懂其设计思路的朋友,欢迎在评论区留言,在此感谢
let old = 0; // 设定全局变量用于保存上一次触发的时间
function throttle(event, wait) {
return function (...args) {
if (new Date() - old >= wait) {
fn.apply(this, args);
old = new Date(); // 记录当前触发的时间
}
}
}
定时器方式
此种方式的缺点是第一次触发事件时不会执行,也曾参详过知乎和CSDN上的博客,发现同样是把timer变量放入了函数内部导致调用时被重置。当然,如有更好的实现方式,欢迎留言指正,感谢
let timer = null;
function throttle(event, wait) {
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
event.apply(this, args);
}, wait)
};
因为两种方式本身都有缺陷,为解决这种缺陷,可以把两者结合起来使用
let old = 0, timer = null;
function throttle(event, wait) {
return function (...args) {
if (new Date() - old >= wait) {
fn.apply(this, args);
old = new Date();
} else {
clearTimeout(timer);
timer = setTimeout(() => {
event.apply(this, args);
}, wait)
}
}
}
总结
防抖函数和节流函数本身原理并不复杂,但难在如何设计一个功能完备且健壮性高的函数
在本文中,我也有许多不解之处,经过多方查看,最终勉强算是解决了这个问题。由此,坚持就是胜利这句话还是有可取之处的,在后续的技术分享博客中,我将贯彻这一精神,望各位能够支持,感谢