1.理解
throttle:限定函数在一定间隔时间内只执行一次。
debounce:某个函数在某段时间内无论被触发多少次,都只执行最后一次。
这样看这两种概念比较迷惑,我们通过一个例子来理解这两个概念。
节流例子:
比如过安检,在开始安检后,在一定时间(10s)内只允许一个乘客过安检,以配合安检人员完成工作。throttle的点在于“一定时间内,限制动作只执行一次”
防抖例子:
比如坐电梯的时候,如果电梯检测有人进来的,就会再重新等待10s,如果这10s内,电梯又检测到有人进来了,电梯将重新等待10s,直到10s过后没有人进来,电梯才会关闭开始运行。debounce的点在于“一个事件发生一定时间之后,才执行特定动作”
然后再结合一张原理图来理解:

2.适用场景
直到节流和防抖的原理后,那么他们分别使用于什么样的场景呢?
这两种实现都是基于前端性能优化考虑的。比如说对于频繁操作或者频繁触发的事情,需要做一些限制,如果不做限制,有可能在1s内执行很多次,会消耗计算机的性能。
节流场景:
- scroll滚动过程中需要监听滚动条的位置
- 防止高频点击事件
防抖场景:
- 搜索框输入。只需要用户最后一次输入完,再发送请求
- 浏览器矿口大小改变,只需要调整之后再执行代码
当然适用场景还是需要结合实际需求来决定的。不过不管用哪一种,都是对性能的一种优化。
3.throttle的实现
实现方案
第一种时间戳来判断是否已到执行时间
缺点:
首次没有执行
事件触发结束后,无法响应回调(trailing:true无效)
var throttle = function(fn, delay = 1000) {
var pre = new Date();
return function() {
var now = new Date();
if (now - pre > delay) {
fn.apply(this, arguments);
pre = now;
}
};
};var fn = () => { console.log("函数被调用");};var throttleHandler = throttle(fn, 2000);var start = new Date();var interVal = setInterval(() => { var end = new Date(); console.log("间隔时间", end - start); throttleHandler();}, 1000);setTimeout(() => { clearInterval(interVal);}, 7000);
//运行结果/*
间隔时间 1000
间隔时间 2001
函数被调用
间隔时间 3001
间隔时间 4001
函数被调用
间隔时间 5000
间隔时间 6001
函数被调用
间隔时间 7000
*/
第二种利用定时器来实现
缺点:
首次没有执行
事件触发结束后,必然会响应回调(trailing:false无效)
var throttle = function(fn, delay) {
var timer = null;
return function() {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this.arguments);
timer = null;
}, delay);
};
};用一个例子结合图片解析这两种实现方式的缺点:
var throttleHandler = throttle(fn, 2000);
var start = new Date();
var interVal = setInterval(() => {
var end = new Date();
console.log("间隔时间", end - start);
throttleHandler();
start = end;
}, 1000);
setTimeout(() => {
clearInterval(interVal);
}, 7000);
//运行结果
/*
间隔时间 1001
间隔时间 2001
间隔时间 3001
函数被调用
间隔时间 4001
间隔时间 5001
间隔时间 6000
函数被调用
间隔时间 7000
函数被调用
*/根据执行结果我们可以分析到:
setinterval是从1000毫秒开始的,然后隔2000毫秒(也就是3000毫秒),执行一次回调。然后4000毫秒的时候又执行了setinterval重新设置settimeout为2000,也就是6000毫秒之后函数再次被触发。但是7000毫秒的时候,回调被中断了,但是最后一次还是会执行。

结合上面两种实现方式来看,利用时间戳应该是更合理的,但是利用时间戳的话,当事件结束后,是没有办法响应的,即不适合trailing=true的情况。
underscore则是结合了这两种方式,实现了更多的可配置项。
underscore简易版本实现方法(结合时间戳和settimeout)
var throttle = function(fn, delay, option = {}) {
//pre为0表示第一次需要执行,因为now-pre>delay肯定满足,其他情况表示不需要执行
var pre = 0;
var timer = null;
var isLeading = option.leading === false ? false : true;
var isTrailing = option.trailing === false ? false : true;
return function(...args) {
let now = +new Date();
//第一次不需要执行的情况 pre = now
if (!pre && !isLeading) {
pre = now;
}
//第一次需要执行 now - pre > delay
//或者间隔事件超过了delay
if (now - pre >= delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
pre = now;
fn.apply(this, args);
} else if (isTrailing && !timer) {
timer = setTimeout(() => {
pre = isLeading ? +new Date() : 0;
fn.apply(this, args);
timer = null;
}, delay - (now - pre))
}
}
};默认情况:
var fn = () => {
console.log("函数被调用");};
var throttleHandler = throttle(fn, 2000);
var start = new Date();
var interVal = setInterval(() => {
var end = new Date();
console.log("间隔时间", end - start);
throttleHandler();
}, 1000);
setTimeout(() => {
clearInterval(interVal);
}, 8000);
//执行结果
/*
间隔时间 1001
函数被调用
间隔时间 2001
间隔时间 3001
函数被调用
间隔时间 4002
间隔时间 5001
函数被调用
间隔时间 6001
间隔时间 7001
函数被调用
间隔时间 8003
函数被调用
*/leading:false测试代码
var fn = () => {
console.log("函数被调用");};
var throttleHandler = throttle(fn, 2000, { leading: false });
var start = new Date();
var interVal = setInterval(() => {
var end = new Date();
console.log("间隔时间", end - start);
throttleHandler();
}, 1000);
setTimeout(() => {
clearInterval(interVal);
}, 8000);
//执行结果
/*
间隔时间 1001
间隔时间 2001
间隔时间 3001
函数被调用
间隔时间 4001
间隔时间 5002
函数被调用
间隔时间6000
间隔时间 7001
函数被调用
间隔时间 8001
函数被调用
*/trailing:false测试代码
var fn = () => {
console.log("函数被调用");
};
var throttleHandler = throttle(fn, 2000, { trailing: false });
var start = new Date();
var interVal = setInterval(() => {
var end = new Date();
console.log("间隔时间", end - start);
throttleHandler();
}, 1000);
setTimeout(() => {
clearInterval(interVal);
}, 8000);
//执行结果
/*
间隔时间 1000
函数被调用
间隔时间 2000
间隔时间 3000
函数被调用
间隔时间 4000
间隔时间 5000
函数被调用
间隔时间 6001
间隔时间 7000
函数被调用
间隔时间 8000
*/
debounce实现
debounce就相对于简单一点。实现原理也是用settimeout,函数执行一次时设定一个定时器,之后调用的时候,发现已经有定时器就了,就清空当前定时器,并重新设置一个定时器。如果存在没有被清空的定时器,当定时器计时结束后触发函数
定时器实现
var debounce = function(fn, delay = 500) {
let timer = null;
return function(...args) {
if (timer) {
clearTimeout = timer;
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
};测试代码
var fn = () => {
console.log("函数被调用");
};
var debounceHandler = debounce(fn, 2000);
var start = new Date();
var interVal = setInterval(() => {
var end = new Date();
console.log("间隔时间", end - start);
debounceHandler();
}, 1000);
setTimeout(() => {
clearInterval(interVal);
}, 3000);
//执行结果
间隔时间 1001
间隔时间 2001
间隔时间 3001
函数被调用
首次被执行的实现
var debounce = function(fn, delay = 500, immediate) {
let timer = null;
return function(...args) {
if (timer) {
clearTimeout(timer);
}
if (immediate && !timer) {
fn.apply(this, args);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
};测试代码
var fn = () => {
console.log("函数被调用");
};
var debounceHandler = debounce(fn, 2000,true);
var start = new Date();
var interVal = setInterval(() => {
var end = new Date();
console.log("间隔时间", end - start);
debounceHandler();
}, 1000);
setTimeout(() => {
clearInterval(interVal);
}, 3000);
//执行结果
间隔时间 1001
函数被调用
间隔时间 2000
间隔时间 3000
函数被调用