防抖和节流

最近经常遇到“防抖”和“节流”这两个概念。作为前端执行优化中比较常用的手法,今天,我就整理一下这两个概念,算作一个备忘。

先说是什么

防抖(Debounce)和节流(throttle)都是用来控制同一函数执行频率的技巧,避免极其频繁的调用引发的性能问题(惯用场景,后面再说),在技巧上两者有一点微妙的区别。

简单理解
  • 防抖,在同一函数被频繁调用的情况下,根据同一函数两次被调用之间的时间间隔判断是否要执行该函数,当时间间隔大于我们预先的设定值,则认为是没有频繁调用,可以执行该函数,否则不予以执行。或者可以认为是,将第一次和最后一次之间的所有函数调用(过渡频繁)都集中在最后一次统一执行,其他调用执行屏蔽掉。

  • 节流,在同一函数被频繁调用的情况下,限制函数周期性的被调用执行,即,每隔一定时间(预先设定)保证调用执行一次,周期内的其他调用执行被屏蔽掉。

举个例子

如果把电梯从 1 楼 到 20 楼认为是一次函数的执行(需要花费一定时间)每一次进来一个人被认为会触发一次函数调用,那在没有防抖和节流的情况下,应该是每次进来一个人都会触发一次函数调用,然后电梯带着这个人从 1 楼到 20楼,这显然是不高效的。

  • 防抖,如果前后两个人进电梯时间间隔小于或等于 10 秒钟,那等一下后面那个人,依次类推,直到有一个人进电梯的时间比他前面的人晚了 10 秒以上,那么就不等后面那个人,电梯就带着此时里面所有的人上去(不考虑电梯容量),否则会一直等下去。

  • 节流,在防抖的情况下考虑,如果不断有人进来,且时间间隔不大于 10 秒,那电梯就一直等着不上去,这样在某些情况下也是不行的。节流则保证,不管怎样,电梯每 8 秒上去一次,周期性的,不再等后面的人了。

再说实现

防抖和节流本质是控制函数执行的频率,所以,防抖和节流本身作为一个高阶函数,其参数之一一定有一个是待控制的函数fn,返回值也应该是一个函数,就是将待控制函数经过防抖/节流处理之后,再返回。这里用setTimeout来简单实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 防抖函数
* @param {Function} fn 待控制函数
* @param {Number} delta 预设时间,两次函数调用时间超出 delta 则执行函数调用
* @param {Object} context 自定义执行上下文
* @return {Function} 返回函数
*/
function debounce(fn, delta, context){
var debounceId = null;
return function(){
clearTimeout(debounceId);
var args = arguments; //fn 可以传参
debounceId = setTimeout(function(){
// delta 时间之后调用 fn
// 如果函数触发时间间隔小于 delta
// 则上一次的 debounceId 会被clearTimeout,不再调用,重新生成当前 debounceId
fn.apply(context, args);
}, delta);
}
}

/**
* 使用demo
*/

function log(){
console.log("log");
}
//鼠标移动就会触发 debounce 函数,但是 debounce 会控制 log 的执行
window.onmousemove = debounce(log, 1000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 节流函数
* @param {Function} fn 待控制函数
* @param {Number} delta 周期执行的时间间隔,保证频繁触发时,该时间间隔内必执行一次
* @param {Object} context 执行上下文
* @return {Function} 返回函数
*/
function throttle(fn, delta, context){
var safe = true;
return function(){
var args = arguments;
if(safe){// safe == true 表示函数调用
fn.apply(context, args);
safe = false;//delta 时间之前不可以执行下一次
setTimeout(function(){//每次调用执行完方法之后,就设置下次 safe == true的时间为delta之后
safe = true;
}, delta);
}
}

}

function throttleLog(){
console.log("throttleLog");
}

//鼠标移动时间绑定了经过 throttle 处理过的 throttleLog 函数,每次移动就会触发
window.onmousemove = throttle(throttleLog, 1000);

immediate 函数

immediate 是 debounce 的一个升级(精确)版本。和 debounce 的区别是,debounce 在 setTimeout 内执行函数,也就是两次函数触发时间间隔 delta 时间之后才执行,而 immediate 则认为是设置当前函数是可执行,然后,经过 delta 时间之后,该可执行有效,则立马执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* immediate 函数
* @param {Function} fn 待处理函数
* @param {Number} delta 时间间隔
* @param {Ojbect} context 执行上下文
* @return {Function} 返回函数
*/
function immediate(fn, delta, context) {
var timeoutID = null;
var safe = true;

return function() {
var args = arguments;
if (safe) {//safe == true 执行函数
fn.call(context, args);
safe = false;
}
clearTimeout(timeoutID); //将 delta 时间间隔内的 safe == true 清除掉,不让函数执行
timeoutID = setTimeout(function() {
safe = true; // delta 时间后将 safe 设置为 true 表示可以执行
}, delta);
};
}

function immediateLog(){
console.log("immediateLog");
}

//鼠标移动时间绑定了经过 immediate 处理过的 immediateLog 函数,每次移动就会触发
window.onmousemove = immediate(immediateLog, 1000);

惯用场景

我们知道,JavaScript 是遵循事件驱动的,一些行为(事件)会触发一些响应(回调),这个被称为事件流,如果行为过快,就会频繁触发响应,如果,回调处理的时间超过了事件触发的间隔事件,因为 JavaScript 事件处理是单线程的,所以就会紊乱,如果涉及到 UI 交互的话,视觉感受就是持续卡顿。
因此,防抖和节流技术通常用在事件处理上,比如,resize、mousemove、click、keypress等可能频繁触发的事件上。

PS: 现代浏览器的帧速率通常为 60fps,就是说 16.7ms 为 1 帧,意味着给我们处理回调响应的时间为 16.7ms,如果超过这个时间过大,则表现为性能不佳,如果此时该回调事件又被触发了多次,都在等待回调,则浏览器就凌乱了,业务逻辑上也有可能出问题。所以,话说回来了,防抖和节流就是控制整个事件被触发的次数的。

PS2: 节流的requestAnimationFrame实现,参看这里

参考文档

Loading comments box needs to over the wall