0%

函数节流和防抖

函数节流和防抖

窗口的resize、scroll、输入框内容校验等操作时,如果这些操作处理函数是较为复杂或页面频繁重渲染等操作时,在这种情况下如果事件触发的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)throttle(节流)的方式来减少触发的频率,同时又不影响实际效果。

函数防抖(debounce)

当持续触发事件时,debounce 会合并事件且不会去触发事件,当一定时间内没有触发再这个事件时,才真正去触发事件,只会触发最后一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function debounce(callback, delay) {
var timer = null, context, args;
return function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
}
}
window.onscroll = debounce(function (e) {
console.log("触发滚动", e)
}, 500)

立刻执行

不希望非要等到事件停止触发后才执行,希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

加个 immediate 参数判断是否是立刻执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function debounce(callback, delay, immediate) {
var timer = null, context, args;
return function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
if (immediate) {
// 判断绑定的函数是否已经执行了 如果已经执行过,不再执行
var didRun = !timer;
if (didRun) {
//没有执行过,立刻执行
callback.apply(context, args);
}
}
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
}
}

返回值

此时注意一点,就是绑定的函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再return 的时候,值将会一直是undefined,所以我们只在 immediatetrue 的时候返回函数的执行结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function debounce(callback, delay, immediate) {
var timer = null, context, args;
var result;
return function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
if (immediate) {
// 判断绑定的函数是否已经执行了 如果已经执行过,不再执行
var didRun = !timer;
if (didRun) {
//没有执行过,立刻执行
result = callback.apply(context, args);
}
}
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
// 返回值
return result;
}
}

取消

最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦。

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
function debounce(callback, delay, immediate) {
var timer = null, context, args;
var result;
var debounced = function (e) {
// 绑定this
context = this;
// 获取参数
args = arguments;
clearTimeout(timer);
if (immediate) {
// 判断绑定的函数是否已经执行了 如果已经执行过,不再执行
var didRun = !timer;
if (didRun) {
//没有执行过,立刻执行
result = callback.apply(context, args);
}
}
timer = setTimeout(() => {
callback.apply(context, args)
}, delay);
// 返回值
return result;
}

// 取消防抖
debounced.cancel = function () {
// 清除timeout
clearTimeout(timeout);
// 重置timeout
timeout = null;
};

return debounced;
}

函数节流

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。

我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

使用时间戳

使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function throttle(callback, delay) {
var previous = 0;
var context, args;
return function () {
// 绑定this
context = this;
// 获取参数
args = arguments;
var now = new Date().getTime();
if (now - previous > delay) {
previous = now;
callback.apply(context, args);
}
}
}

使用定时器

接下来,我们讲讲第二种实现方式,使用定时器。

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,重置定时器,这样就可以设置下个定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function throttle(callback, delay) {
var timeout;
var previous = 0;

return function () {
// 绑定this
context = this;
// 获取参数
args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
// 重置定时器
timeout = null;
callback.apply(context, args)
}, delay)
}
}
}

比较使用时间戳,使用定时器两个方法:

第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件

双剑合璧

想要控制第一次是否立刻执行,事件停止触发后是否会再执行一次事件。

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

  • leading:false 表示禁用第一次执行
  • 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
33
34
35
36
37
38
39
40
41
42
43
44
45
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};

var later = function () {
// 如果禁用第一次立刻执行 设置previous为0,throttled函数remaining会确保大于0 throttled函数会执行else if条件语句里面的内容,从而达到节流的效果,如果没有禁用第一次立刻执行,需要重置previous为当前时间
previous = options.leading === false ? 0 : new Date().getTime();
// 执行完重置timeout
timeout = null;
// 执行函数
func.apply(context, args);
if (!timeout) context = args = null;
};

var throttled = function () {
var now = new Date().getTime();
// 触发第一次操作时,默认会第一次立刻执行,第一次执行remaining会小于0,程序会执行 if条件语句的内容,快速执行第二次点击previous = now;remaining会大于0,会执行else if条件语句里面的内容
// 触发第一次操作时,如果禁用第一次立刻执行,当前previous为0,将previous设置为now,remaining会大于0 程序会执行else if条件语句里面的内容
if (!previous && options.leading === false) previous = now;
// 获取触发函数剩余的时间
var remaining = wait - (now - previous);
// 绑定this
context = this;
// 绑定arguments
args = arguments;
if (remaining <= 0 || remaining > wait) {
// 清除timeout
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
// 重置时间
previous = now;
// 执行函数
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 事件停止触发后会再执行一次事件
// 通过计时器setTimeout,设置remaining时间后执行事件触发,达到节流
timeout = setTimeout(later, remaining);
}
};
return throttled;
}

总结

  • 函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。
  • 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。

应用场景

  • debounce
    • search搜索,用户在不断输入值时,用防抖来节约请求资源。
    • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
  • throttle
    • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
    • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

参考

https://juejin.im/post/5b8de829f265da43623c4261

https://juejin.im/post/5b7b88d46fb9a019e9767405?utm_medium=fe&utm_source=weixinqun

https://github.com/mqyqingfeng/Blog/issues/22

https://github.com/mqyqingfeng/Blog/issues/26

-------------本文结束感谢您的阅读-------------