老生常谈节流函数

节流函数

节流函数算是老生常谈了,项目里面必备的基础函数,但你真的用对了吗?深入研究了一下,发现自己用的确实不太对。首先了解一下节流函数的概念

节流函数:顾名思义,像节流阀一样均匀的执行,把密集的重复调用,通过设置定时器,使函数在固定的间隔时间调用,在一段时间内,等时间间隔的调用多次,这里跟的bounce函数一定要区分开。

debounce: 中文意思,防反跳,防止密集的重复请求,即一定时间间隔内的密集请求都丢弃,等待间隔时间后不再请求再去执行,在一段时间内,可能只调用一次。

分清楚这两个概念之后,先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const generateThrottle = function (throttleTime) {
let time = Date.now()
return function (now) {
// 如果没有设置节流时间, 使用默认配置的时间
if (now - time > (throttleTime || AppConfig.THROTTLE_TIME)) {
time = now
return true
}
}
}

let resizeThrottle = generateThrottle()
// 监听窗口resize
window.addEventListener('resize', event => {
// 发布滚动事件
resizeThrottle(Date.now()) && dosomething()
})

这是我们项目里面的节流函数,咋一看没啥问题,直到项目出现了一个很偶现的bug,最后发现是dosomething()这个函数偶尔会不执行引起的。我们再来仔细看一下代码,当时间间隔大于THROTTLE_TIME时函数返回ture,否则就不返回,如果最后一次缩放窗口和上次时间间隔小于THROTTLE_TIME,然后停止缩放,那么这个时候dosomething()就不执行了,最后导致dosomething的状态还停留在上一次缩放,导致bug。所以当你希望停止缩放后再执行一次dosomething(),那么上面这个函数显然是不合理的!

然后我们重写了这个节流函数如下:

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
/**
* 首次调用创建一个定时器,最多每隔 mustRunDelay 时间调用一次该函数
* @param fn 执行函数
* @param delay 时间间隔
* @param mustRunDelay 必然触发执行的时间间隔
* @returns {Function}
*/
var throttle = function(fn, delay, mustRunDelay) {
var timer = null;
var previous;
return function() {
var context = this, args = arguments, now = +new Date();
clearTimeout(timer);
if(!previous) {
previous = now;
}
if(now - previous >= mustRunDelay){
fn.apply(context, args);
previous = now;
} else {
timer = setTimeout(function(){
fn.apply(context, args)
}, delay)
}
}
}
1
2
3
4
function testFn () {
console.log(111111)
}
window.onresize = throttle(testFn, 50, 1000)

这个函数首次调用会创建一个定时器,当重复调用的时候会不断的清空并重置定时器,当时间间隔大于等于mustRunDelay时,调用该函数,并更新时间戳。这样就保证了函数每隔mustRunDelay后重复调用该函数,并且在最后一次触发之后的delay时间后最后一次执行该函数。这个函数就基本能满足我们业务的需求了。如果你想首次调用能够立即执行该函数或者最后一次调用并不触发该函数,那么继续往下看。

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
/**
* 创建并返回一个像节流阀一样的函数,当重复调用函数的时候,最多每隔 wait毫秒调用一次该函数
* @param func 执行函数
* @param wait 时间间隔
* @param options 如果你想禁用第一次首先执行的话,传递{leading: false},
* 如果你想禁用最后一次执行的话,传递{trailing: false}
* @returns {Function}
*/
function throttle (fn, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};

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

return 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;
result = fn.apply(context, args);
if(!timeout) context = args = null;
} else if(!timeout && options.trailing !== false){
timeout = setTimeout(later, remaining)
}
return result;
}
}
1
2
3
4
function testFn () {
console.log(111111)
}
window.onresize = throttle(testFn, 200, {leading:false})

这个节流函数就相当的完美了,默认是首次立即执行fn,最后一次调用之后的wait时间后执行,当重复调用的时候,最多每隔wait时间间隔调用一次fn。也可以通过传参来禁用第一次执行和最后一次执行。

debounce函数

然后我们再来看看高程上面的节流函数,函数每次调用的时候先清空定时器,再设置一个新的定时器,当窗口缩放的时候会不断的触发throttle,这个时候实际上fn只有在最后一次触发throttle后100ms执行,而且只执行一次。所以这个并不是真正意义上面的节流函数。而是一个debounce函数,丢弃一些密集重复的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle (method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function(){
method.call(context)
}, 100)
}

function fn () {
console.log(111111)
}
window.onresize = function(){
throttle(fn);
}

下面这个函数写法不一样,其实结果也是一样,只执行一次

1
2
3
4
5
6
7
8
9
10
11
let throttle = function(fn, delay){
let timer = null;
return function(){
let content = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(function(){
fn.apply(content, args)
}, delay)
}
}
1
2
3
4
function testFn () {
console.log(111111)
}
window.onresize = throttle(testFn, 1000)

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
34
35
36
/**
* 防反跳 fn函数在最后一次调用时刻的wait毫秒之后执行!
* @param fn 执行函数
* @param wait 时间间隔
* @param immediate 为true,debounce会在wait 时间间隔的开始调用这个函数
* @returns {Function}
*/
function debounce(fn, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function(){
var last = new Date().getTime() - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if(!immediate) {
result = fn.apply(context, args);
context = args = null;
}
}
}
return function() {
context = this;
args = arguments;
timestamp = new Date().getTime(); //每次触发时更新
var callNow = immediate && !timeout;
if(!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = fn.apply(context, args);
context = args = null;
}
return result;
}
}
1
2
3
4
function testFn () {
console.log(111111)
}
window.onresize = debounce(testFn, 300)

1.当immediate 为true时,立即执行fn,wait时间间隔内频繁调用,不断的重置定时器,并不会触发fn,wait时间之后通过将定时器清空,等待wait间隔之后再次调用,则立即执行fn。之后如此循环往复。

使用场景:比如微博下拉刷新,首次下拉请求,中间频繁的下拉并不会触发请求,一定时间后再刷新会重新触发

2.当immediate 为false,不会立即执行该函数,wait 时间间隔内频繁调用,不断重置定时器,wait 时间间隔后,如果无触发,则清空定时器,触发fn,只执行一次fn;

使用场景:用户输入验证,不在输入过程中处理,停止输入后进行验证