javaScript之节流

节流

节流就是当你持续触发事件时,每隔一段时间只会执行一次事件。
根据首次是否执行以及结束后是否执行,效果也不相同,实现的方式也有所不同。这里使用leading代表首次是否执行,trailing代表结束后是否在执行一次。
目前节流有两种主流实现方式:
1. 使用时间戳
2. 设置定时器

使用时间戳

使用时间戳也就是说,当事件被触发的时候,我们取出当前的时间戳,然后减去之前的时间戳,如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于就不执行。
代码形式如下:

1
<div id="container"></div>
1
2
3
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
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
function throttle(func, wait) {
var args,context;
var previous = 0;

return function(){
context = this;
args = arguments;
var now = +new Date();
// +new Date() 会隐式的把Date的字符串转化为数字
//Fri Aug 02 2019 21:36:01 GMT+0800 (中国标准时间) ===> 1564752954774
if(now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}

// 使用如下
var count = 1;
var container = document.getElementById('container');

function getUserAction() {
container.innerHTML = count++;
};
container.onmousemove = throttle(getUserAction, 1000);

效果如下:
img
当鼠标移入的时候,事件立即执行,相比没使用节流之前鼠标移动一下就会触发一次,现在是当鼠标移入时立即触发一次,在设定的1秒时间内无论怎么移动都不会再次执行,

使用定时器

第一种使用了时间戳,现在换成定时器来试试

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(func, wait) {
var context, args, timeout;
return function(){
context = this;
args = arguments;
if(!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args);
}, wait)
}
}
}

比较下这两种实现的区别:
1. 第一种事件会立刻执行,第二种事件会在n秒后第一次执行。
2. 第一种事件停止触发后没有办法再执行事件,第二种停止触发n秒后,最后一次执行事件。

综合上述两种方式,我们需要一个:触发事件会立即执行,停止触发后n秒还会最后执行一次的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
function throttle(func, wait) {
var timeout, context, args, result;
var previous = 0;

var later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};

var throttled = function() {
var now = +new Date();
//下次触发 func 剩余的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}

img
上述的功能需求已经完成。

优化

需求: 有时我们也希望无头有尾,或者有头无尾。根据传进来的值判断到底是那种效果。
这里约定:

  1. leading: false 表示禁用第一次执行
  2. trailing false 表示禁用停止触发的回调
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
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};

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

var throttled = function() {
var now = new Date().getTime();
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;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}

取消

在debounce中,添加一个cancel方法

1
2
3
4
5
throttle.cancel = function(){
clearTimeout(timeout);
previous = 0;
timeout = 0;
}

注意

undersocre的实现中有这样一个问题:
leading: falsetrailing: false 不能同时设置

如果同时设置的话,当你将鼠标移除的时候,因为trailing设置为false,停止触发的时候不会设置定时器了,所以只要再过了设置的时间,在移入的话,就会立刻执行,就违反了leading:false,bug就出来了,所以,这个throttle只有三种用法:

1
2
3
4
5
6
7
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});