函数节流(throttle)与函数去抖(debounce)

以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。

  1. window对象的resize、scroll事件

  2. 拖拽时的mousemove事件

  3. 射击游戏中的mousedown、keydown事件

  4. 文字输入、自动完成的keyup事件

实际上对于 window 的 resize 事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了 debounce 和 throttle 两种解决办法。

什么是debounce

[dɪ’baʊns] 防止反弹,即如果用手指一直按住一个弹簧,它将不会弹起直到你松手为止。

也就是说当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间。

如果该动作持续的触发,那么该函数只会在最后一次触发之后再延迟delay的时间才会执行一次。

接口定义:

1
2
3
4
5
6
/**
* @param fn {function} 请求关联函数,实际应用需要调用的函数
* @param delay {number} 空闲时间,单位毫秒
* @return {function} 返回客户调用函数
*/
debounce(fn, delay) {}

简单实现:

1
2
3
4
5
6
7
8
9
10
var debounce = function(fn, delay){
var timer;
return function () {
var ctx = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(ctx, args)
}, (delay ? delay : 300));
};
}

什么是throttle

[ˈθrɒtl] 节流阀,即如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。

也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。

如果该动作持续的触发,那么该函数会在每经过delay时间之后就会执行一次。最终会执行n次, n = 总触发时间/delay。

接口定义:

1
2
3
4
5
6
/**
* @param fn {function} 请求关联函数,实际应用需要调用的函数
* @param delay {number} 延迟时间,单位毫秒
* @return {function} 返回客户调用函数
*/
throttle(fn, delay) {}

简单实现:

1
2
3
4
5
6
7
8
9
10
var throttle = function(fn, delay) {
var last = 0;
return function() {
var curr = + new Date();
if (curr - last > delay){
fn.apply(this, arguments)
last = curr;
}
}
}

underscore实现

debounce

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
31
32
33
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function(func, wait, immediate) {
var timeout, result;

var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};

var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}

return result;
});

debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};

return debounced;
};

throttle

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};

var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};

var throttled = function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};

throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};

return throttled;
};