聊聊函数防抖和函数节流

关于函数防抖和函数节流,相信你已经听过了很多遍了。但是两者的差别,你能快速准确的说出来吗?下面,我们就来聊一聊函数防抖和函数节流的本质区别和实现。

函数防抖和函数节流的基本概念

在博客里看到过这样一个经典比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。假设电梯有两种运行策略 debounce 和 throttle,超时设定为15秒,不考虑容量限制。
函数防抖(debounce) 策略的电梯。如果电梯里有人进来,等待15秒。如果又人进来,15秒等待重新计时,直到15秒超时,开始运送。
函数节流(throttle) 策略的电梯。保证如果电梯第一个人进来后,15秒后准时运送一次,不等待。如果没有人,则待机。

即:

函数防抖: 在调用操作一定时间后,才会执行该方法,如果在该段时间内,再次调用该方法,则重新计算该执行时间间隔。
函数节流: 调用方法之前,预先设定一个执行周期,当调用动作的时间节点大于等于这个执行周期,才执行该方法,然后进入下一个新的执行周期。

简而言之,函数防抖和函数节流都是为了降低方法的执行频率,防止函数触发频率过高,导致浏览器响应速度跟不上触发频率,而出现浏览器假死、卡顿等现象,优化用户体验。

函数防抖

上面讲到了函数防抖的概念,下面我们就用代码实现一个函数防抖方法的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function debounce(fn, delay) {
var timer;
return function () {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout( function () {
fn.apply(context, args);
}, delay)
}
}
function debounce(fn, delay) {
let timer;
return function(...args) {
let context = this;
let _args = args;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, _args);
}, delay);
}
}

上述方法主要利用了闭包,闭包会被连续的调用,我们在闭包内部对 timer 进行了操作,限制了 fn 的执行次数。
但是上述方法也有一个缺点,如果我们在滚动页面时,触发一个方法执行,页面如果一直处于滚动状态,那么添加的方法,就一直无法触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function debounce(fn, delay, time){
var previous = null; //记录上一次运行时间
var timer = null;

return function () {
var now = +new Date();
if(!previous) previous = now;
//当上一次执行时间间隔与当前时间差大于设置的执行时间间隔,主动执行一次
if(now - previous > time){
clearTimeout(timer);
fn();
previous = now;//执行后,记录当前的执行时间
}else{
clearTimeout(timer);
timer = setTimeout(function(){
fn();
}, delay)
}
}
}

函数节流

说起函数就留,就不得不提起当初看红宝书(javascript高级程序设计)时,给列举了这样一个方法:

1
2
3
4
5
6
function throttle (method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function () {
method.call(context)
})
}

是不是有点眼熟呢?这个分明是函数防抖的一种实现啊,不知道为什么这么大的一个失误,竟然一直没有被改正。

那么真正的函数节流该怎么实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function throttle (fn, threshhold){
var last, timer
threshhold || (threshhold = 250)
return function () {
var context = this;
var args = arguments;
var now = +new Date();
//如果距离上次执行 fn 方法的时间小于 threshhold,就clearTimeout
if(last && now < last + threshhold) {
clearTimeout(timer);
//保证在当前时间区间结束后,再执行一次 fn
timer = setTimeout(function () {
last = now;
fn.apply(context, args)
}, threshhold)
} else {
last = now;
fn.apply(context, args)
}
}
}

上述代码中,关键在于if else中,每次回调执行后,需要保存执行函数的时间戳,为了计算以后的事件触发回调时与之前执行回调函数的时间戳的间隔,从而判断是否需要执行回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, threshold){
var flag = true;
return function () {
var context = this;
var args = arguments;
if(!flag) return;
flag = false;
setTimeout(function (){
fn.apply(context, args)
flag = true
}, threshold)
}
}

上面这个为我日常使用的简化版throttle,通过一个 flag 标志位实现对时间间隔的判断。

参考