0%

函数防抖与节流

在前端开发的过程中,经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。所以诞生了 防抖与节流函数

防抖(debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

页面部分

1
2
3
4
5
6
7
8
9
10
<div id="box" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ddd;font-size:80px;"></div>
<script>
let num = 1;
let content = document.getElementById('box');

function count() {
content.innerHTML = num++;
};
content.onmousemove = count;
</script>

在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。效果如下
连续变化
可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。

防抖函数分为非立即执行版和立即执行版。

非立即执行版:

1
2
3
4
5
6
7
8
9
10
11
// 非立即执行
function debounce(func, wait){
let timeout;
return function(){ // 返回函数的 this 指向为当前的操作对象;
let that = this;
timeout && clearInterval(timeout);
timeout = setTimeout(function(){
func.apply(that, arguments);
}, wait)
}
}

非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

使用上述绑定 mousemove 事件的例子,通过上面的防抖函数

1
content.onmousemove = debounce(count,1000);

防抖操作
可以看到,在触发事件后函数 1 秒后才执行,而如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。

上述防抖函数的代码还需要注意的是 this 和 参数的传递

1
2
let context = this;
let args = arguments;

防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。

立即执行版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 带有首次执行 immediate
function debounce(func, wait){
let timeout;
return function(){
let that = this;
let arg = arguments;

timeout && clearTimeout(timeout);

let nowRun = !timeout;

timeout = setTimeout(()=> timeout = null, wait)
nowRun && func.apply(that, arg)
}
}

立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

立即执行
我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,最终实现防抖:

最终版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @desc 函数防抖
* @param func 函数
* @param wait 延迟执行毫秒数
* @param immediate true 表立即执行,false 表非立即执行
*/
function debounce(func, wait, immediate){
let timeout;
return function(){
let that = this;
let arg = arguments;
timeout && clearTimeout(timeout);
if(immediate){
let nowRun = !timeout;
timeout = setTimeout(()=> timeout = null, wait)
nowRun && func.apply(that, arg)
} else {
timeout = setTimeout(()=> func.apply(that, arg) , wait)
}
}
}

节流(throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:

1
2
3
4
5
6
7
8
9
10
11
12
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}

使用方式如下:

1
content.onmousemove = throttle(count,1000);

效果如下:
时间戳
可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。

定时器版:

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

}
}

使用方式同上
定时器版
可以看到,在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。

我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候

最终版:

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
/**
* @desc 函数节流
* @param func 函数
* @param wait 延迟执行毫秒数
* @param type 1 表时间戳版,2 表定时器版
*/
function throttle(func, wait ,type) {
if(type===1){
let previous = 0;
}else if(type===2){
let timeout;
}
return function() {
let context = this;
let args = arguments;
if(type===1){
let now = Date.now();

if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}else if(type===2){
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}

参考文章:
https://github.com/mqyqingfeng/Blog/issues/22

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

函数防抖与节流

underscore 源码

------ 本文结束------