Fork me on GitHub

js防抖与节流

为什么要防抖

在前端开发中会遇到一些频繁的事件触发,比如:

  1. window 的 resize、scroll
  2. mousedown、mousemove
  3. keyup、keydown
    ……
1
2
3
4
5
6
7
8
9
10
11
12
<!-- index.html -->
...
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
...
<body>
<div id="container"></div>
<script src="debounce.js"></script>
</body>
1
2
3
4
5
6
7
8
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
container.onmousemove = getUserAction;

因为这个例子很简单,所以浏览器完全反应的过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。

为了解决这个问题,一般有两种解决方案:

  1. debounce 防抖
  2. throttle 节流

防抖原理是什么

防抖的原理就是:你尽管触发事件,但是我一定在事件触发n秒后才执行,如果你在一个事件触发的n秒内又触发了这个事件,那我就以新的事件的时间为准,n秒后才执行,总之,就是要等你触发完事件n秒内不再触发事件,我才执行。

防抖的实现

第一版

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

还是开始的例子测试:

1
container.onmousemove = debounce(getUserAction, 1000)

现在是移动完1000ms之后才执行getUserAction事件:

绑定this

如果我们在getUserAction函数中打印this,在不使用防抖的时候,this的值为:

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

但是用了刚才实现的防抖函数之后,因为setTimeout是挂载在window对象上的,所以getUserAction中的this会指向Window对象。所以我们需要将this指向正确的对象。

1
2
3
4
5
6
7
8
9
10
11
// 第二版
function debounce(func, wait) {
var timeout
return function() {
var context = this
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context)
}, wait)
}
}

现在this已经可以正确指向了。

event对象

事件监听的回调函数的第一个参数是事件对象,现在我们要把这个对象传给防抖函数中的func

1
2
3
4
5
6
7
8
9
10
11
12
// 第三版
function debounce(func, wait) {
var timeout
return function() {
var context = this
var args = arguments
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(this, args)
}, wait)
}
}

立即执行

上面封装的防抖函数属于延迟执行的防抖函数,只在事件结束后最后调用。比如使用搜索引擎输入问题的时候,我们希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数。

考虑一个新的需求:需要事件触发后立刻执行函数,然后停止触发事件n秒后才能重新触发。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 第四版
function debounce(func, wait, immediate) {
var timeout = null
return function() {
var context = this
var args = arguments
if (timeout) clearTimeout(timeout)
if (immediate) {
// 立即执行
if (!timeout) func.apply(context, args)
timeout = setTimeout(function(){
timeout = null
}, wait)
}
else {
// 延迟执行
timeout = setTimeout(function() {
func.apply(context, args)
}, waite)
}
}
}

返回值

getUserAction可能是有返回值的,所以我们也要返回函数的执行结果,但是当防抖函数是延迟执行的时候,因为使用了setTimeout,我们return的结果会一直是undefined,所以我们只在立即执行的防抖函数下返回函数的执行结果。

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 = null, result = null
return function() {
var context = this
var args = arguments
if (timeout) clearTimeout(timeout)
if (immediate) {
// 立即执行
if (!timeout) result = func.apply(context, args)
timeout = setTineout(function() {
timeout = null
}, wait)
} else {
// 延迟执行
timeout = setTimeout(function() {
func.apply(this, args)
}, wait)
}
return reult
}
}

取消

最后我们希望增加一个能取消防抖,可以立刻重置延迟时间,初始化防抖函数的功能。

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
// 第六版
function debounce(func, wait, immediate) {
var timeout = null, result = null
var debounced = function() {
var context = this
var args = this
if (timeout) clearTimeout(timeout)
if (immediate) {
// 立即执行
if (!timeout) result = func.aplly(context, args)
timeout = setTimeout(function() {
timeout = null
}, wait)
} else {
// 延迟执行
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
return result
}
debounced.cancel = function() {
clearTimeout(timeout)
timeout = null
}
return debounced
}

防抖的出处

JavaScript专题之跟着underscore学防抖

节流

节流的原理

节流的原理很简单:如果你持续触发事件,每隔一段时间,只执行一次事件。

根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。我们用leading代表首次是否执行,trailing代表结束后是否再执行一次。

使用时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 第一版
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}

使用定时器

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

节流的出处

JavaScript专题之跟着 underscore 学节流

undefined