爱客仕-前端团队博客园

JS函数节流策略

函数节流策略

这里以underscore的 throttle 和 debounce 两种函数节流策略先举个例子

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。假设电梯有两种运行策略 throttle 和 debounce ,超时设定为15秒,不考虑容量限制。

  • throttle 策略的电梯。保证如果电梯第一个人进来后,15秒后准时运送一次,不等待。如果没有人,则待机。
  • debounce 策略的电梯。如果电梯里有人进来,等待15秒。如果有人进来,15秒等待重新计时,直到15秒超时,开始运送。

throttle 策略

假如有这么个场景,在窗口resize或页面在scroll时我们要在回调里对页面上做dom操作,如果不加以控制,让回调肆无忌惮执行,那么很有可能会让浏览器崩溃,这显然不是我们想要的。这个问题可以用 throttle 策略解决

看一下underscore的源码,对 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
46
/**
* 返回函数连续调用时,func 每 wait 时间执行一次
*
* @param {function} func 传入函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始边界上的调用,传入{leading: false}。
* 如果想忽略结尾边界上的调用,传入{trailing: false}
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 上次执行时间点
var previous = 0;
if (!options) options = {};
// 延迟执行函数
var later = function() {
// 若设定了开始边界不执行选项,上次执行时间始终为0
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
var now = _.now();
// 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。
if (!previous && options.leading === false) previous = now;
// 延迟执行时间间隔
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口
// remaining大于时间窗口wait,表示客户端系统时间被调整过
if (remaining <= 0 || remaining > wait) {
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;
};
};

所以上面的场景,我们可以这样解决

1
2
3
4
var todo = _. throttle(function(){
// todo
}, 200);
window.addEventListener('resize', todo, false);

debounce 策略

假如有这么个场景,项目某个页面有个搜索框,在用户从键盘输入的时候,就开始搜索,而不是点搜索按钮或者敲回车再去搜索。我们首先想到的可能是给搜索框绑定keyup, keydown, keypress的事件。这个办法本身是没有问题的,但是如果用户很变态的快速输入了几十个字符,那岂不是要向服务器发送几十个请求,这肯定不是我们想要的,这个需求可以用 debounce 策略解决。

我们先看一下keyup, keydown, keypress这三个事件的区别

  • keydown 和 keyup 基本可以捕获标准键盘上所有键,除了截屏键(Prscm),并且可以捕获组合键,但是在获取keyCode时对大小写不敏感,并且获取不到charCode

  • keypress 只能响应字符和数字键,由于中文输入法输完以后最后一个动作是空格或回车,所以该事件对中文不怎么感冒,但是在获取keyCode时对大小写敏感,同一个字符在大写和小写的情况下,获取keyCode的值是不一样的,同时可以获取到charCode

chrome 英文输入法下

事件触发顺序是:keydown -> keypress -> keyup

chrome 中文输入法下

事件触发顺序是:keydown -> keyup 由于keypress无法监听键盘功能键事件,所以keypress不会触发

好了我们回到正题,先看一下underscore的源码,对 debounce 策略的实现

1
_.debounce(function, wait, [immediate])
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
/**
* 返回函数连续调用时,间隔时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 传入函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,调用触发于开始边界而不是结束边界
* @return {function} 返回客户调用函数
*/
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
// 据上一次触发时间间隔
var last = _.now() - timestamp;
// 上次被包装函数被调用时间间隔last小于设定时间间隔wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate && !timeout;
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};

不得不说写得很高级,想的很全面。

所以上面的场景,我们可以这样解决

1
2
3
4
var query = _.debounce(function(){
// 异步查询
}, 200);
document.getElementById('search').addEventListener('keyup', query, false);

其实自己写一个也够用

建议一些高频率触发事件,一定要采取函数节流策略。