javaScript之防抖

防抖

防抖的原理就是:尽管你触发了许多许多次事件,但是每一次事件触发的时候都会清除上次的事件,并以这次触发事件的时间为开端n秒后才进行事件的真正执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
<title>debounce</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>

<body>
<div id="container"></div>
</body>

</html>

以上是测试的html文件

1
2
3
4
5
6
7
8
var count = 1;
var container = document.getElementById('container');

function getUserAction() {
container.innerHTML = count++;
};

container.onmousemove = getUserAction;

以上是JavaScript代码

此测试效果图:
img
可以看到鼠标滑过div,getUserAction事件被触发了很多次,因为当前事件非常简单所以浏览器能够迅速处理掉,但是如果脚本代码有异步代码,请求数据等,浏览器不能马上处理完,就会造成卡顿。

第一版

1
2
3
4
5
6
7
8
9
10
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
}
}

//测试使用
container.onmouseover = debounce(getUserAction, 1000);

下面是使用效果:
img
从图中可以看出,事件的触发被限制了。

this

在getUserAction函数里面console.log(this),如果没有使用debounce函数,this指向的是Dom节点div,但是一旦使用了debounce函数,this变成指向了window对象。
这时我们需要修正this的指向

第二版

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(func, wait) {
var timeout;

return function() {
var context = this;

clearTimeout(timeout);
timeout = setTimeout(function(){
func.apply(context)
}, wait);
}
}
// 现在this可以正确的指向了

event对象

Javascript 在事件处理函数中会提供事件对象event,我们修改getUserAction函数:

1
2
3
4
5
6
7

function getUserAction(e) {
console.log(e); //MouseEvent {isTrusted: true, screenX: 89, screenY: 128, clientX: 85, clientY: 16, …}
container.innerHTML = count++;
}

container.onmousemove = getUserAction;

可以看到,在没有使用debounce函数,打印出来的时MouseEvent对象,
但是在debounce函数中只会打印出undefined。
所以我们来修改下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function debounce(func, wait) {
var timeout;

return function() {
var context = this;
// arguments[0] 对应的值就是MouseEvent
var args = arguments;

clearTimeout(timeout);
timeout = setTimeout(function(){
// 修正this指向,并给了一个event对象
func.apply(context, args);
} ,wait)
}
}

以上代码修复了两个小问题:

  1. this指向
  2. event对象

立即执行

之前的代码都是让事件在最后一次触发的n秒后才执行,现在有一个新的需求就是,要触发事件就立马执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function debounce(func, wait, immediate){
var timeout;

return function(){
var context = this;
var args = arguments;

if(timeout) clearTimeout(timeout)
//是否要立即执行
if (immediate) {
var callNow = !timeout; // 这里和timeout = null 构成一个true|fase标记,只会让它在wait时间内执行一次
timeout = setTimeout(function(){
timeout = null;
},wait)
if (callNow) func.apply(context, args)
}else{
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
}
}
}

这里是效果图:
img

返回值

需要注意的是,getUserAction可能会有返回值,我们也要返回函数的执行结果,当immediate为fals的时候,因为使用了setTimeout,我们将func.apply(context, args)的返回值赋给变量,然后在return的时候会是undefined,所以我们只要在immediate为true的时候返回函数的执行结果。

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
function debounce(func, wait, immediate) {

var timeout, result;

return function () {
var context = this;
var args = arguments;

if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
//只在immediate为true的时候函数的执行结果
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
}
}

取消

最后再来一个需求,希望能取消debounce函数,比如debounce的wait时间是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
function debounce(func, wait, immediate) {

var timeout, result;

var debounced = function () {
var context = this;
var args = arguments;

if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
// 这里是防抖的取消函数
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
// 把自身return出去,在外面就能调用cancel函数了,使用了闭包
return debounced;
}

这里是取消函数的使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var count = 1;
var container = document.getElementById('container');

function getUserAction(e) {
container.innerHTML = count++;
};

var setUseAction = debounce(getUserAction, 10000, true);

container.onmousemove = setUseAction;

document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
})

效果图:
img