事件防抖和事件节流

概述

一些事件被连续触发引起的频繁执行JavaScript代码会导致用户体验较差,如果处理事件的逻辑较为复杂甚至可能会引起页面假死的现象,使用事件防抖(debounce)和事件节流(throttle)可以解决这个问题。

问题的引出

考虑下面的情形,当某个视图处理逻辑是配合浏览器窗口的大小变动而响应执行时,会出现怎样的问题。

1
2
3
window.onresize = e => {
// ...
};

如果事件的处理逻辑相应比较简单,浏览器执行起来较快,好像问题也不大,然而有的时候由于窗口大小被改变,页面中的很多元素大小尺寸也需要跟着改变,这是如果每一个resize事件都要处理,就会消耗较多资源,并且一定程度引起页面卡顿,用于体验不好。

怎样解决这个问题呢?不难发现,window.onresize连续触发之后总有一个结束点,如果能在最后一次响应时执行真正需要的处理逻辑的函数,这样就能避免大量的重复调用。关键的问题是,如何确定连续的函数调用的最后一次调用是什么时候。

由事件频繁触发引起的函数连续调用,无论如何都会有一个时间间隔,为了确定最后一次调用的时间,可以假定一个时间间隔(比如100ms),如果超过这个时间函数没有被调用,则可以判断当前的连续调用已经结束,可以真正开始执行处理函数了。

函数防抖

JavaScript中由于函数调用太过频繁而预先设置一个时间间隔,如果超过这个间隔函数没有被调用,才真正开始执行,否则不执行继续等待之后的调用的设计,被称为函数防抖(debounce)。

一个典型的例子就是impress.js,它是一个在浏览器的展示幻灯片的工具,当打开impress.js的官方案例时,如果调整浏览器窗口的大小会发现,幻灯片的大小只有在窗口真正稳定下来(用户不再调整窗口的大小之后)才会重新适应新的窗口的大小。

Impress.js中的函数防抖

函数防抖的简单实现

为了让目标函数在间隔一定时间之后执行,需要用到的setTimeout函数,但是单一的setTimeout函数只会推迟函数的调用,因此需要一个简单的设置,每次都把之前设置的定时器清除掉。

1
2
3
4
5
6
7
8
9
const debounce = (func, wait = 100) => {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.call(this, ...args);
}, wait);
};
};

debounce函数接收2个参数,第1个是需要防抖的目标函数,第2个是防抖等待的时间wait(默认值100ms)。debounce是一个闭包,它返回的函数有一个私有变量timeout,当函数每次触发时,timeout都被清除一次,然后重新设置一个新的定时器,用于执行防抖目标函数,定时器等待的时间是wait毫秒。

接着测试window.onresize事件,尝试改变浏览器窗口的大小,会发现无论改变窗口的速度多快,只要500ms内又重新改变了窗口,之前触发的逻辑都不会被执行。

1
window.onresize = debounce(e => console.log('hello world'), 500);

除此之外,还可以测试目标函数执行的上下文(this和函数参数),这里可以尝试在目标函数中输出this和e试试,会发现和正常情况下(不适用debounce绑定目标函数)一样,this和函数参数都能被正常使用。

这是因为debounce中返回的是一般的函数声明而没有用箭头函数,而里面的setTimeout却是箭头函数,这样就保证了防抖目标函数被调用时的上下文和正常情况下一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<input type="text">
<script src="./my-debounce.js"></script>
<script type="text/javascript">
const input = document.querySelector('input');
input.addEventListener('input', debounce(function(e) {
console.log(this); // <input type="text">
console.log(e); // InputEvent { ... }
}, 500));
</script>
</body>
</html>

函数节流

除了函数防抖,还有一个相关的概念叫函数节流(throttle)

函数节流就是当目标函数被连续频繁调用时,目标函数并不会每次调用都被执行,而是间隔一个设定好的时间再执行。

鼠标滑过某个区域的时候,如果处理逻辑的计算量或者消耗资源较大,这时使用函数节流可以有效提高用户体验。

函数节流的简单实现

1
2
3
4
5
6
7
8
9
10
const throttle = (func, wait = 50) => {
var lastCall = Date.now();
return function(...args) {
const now = Date.now();
if (now - lastCall > wait) {
func.call(this, ...args);
lastCall = now;
}
};
};

这里创建一个函数节流的简单实现,每次调用都记录一下当前时间,看看是否和上次调用的时间差是否大于指定的时间差,如果大于则调用目标函数,并且记录当前时间为新的上一次调用时间。

同样地,这里也处理好了目标函数的调用上下文(this和参数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css">
html,body {
margin: 0px;
height: 100%;
}
</style>
</head>
<body>
<script src="./my-throttle.js"></script>
<script type="text/javascript">
document.body.onmousemove = throttle(e => {
console.log('hello world');
}, 200);
</script>
</body>
</html>

实现上面的代码,试着在页面上滑动,检查控制台,会发现都是间隔200毫秒左右才输出一次的。

一般来说,间隔的时间应该根据业务的不同也不同,如果时间太大,会使效果间断时间太长,降低用户体验。

总结

简单介绍了一下函数防抖和函数节流的基本,感兴趣的同学可以去看一下lodash的相关实现,lodash的底层有更多优化而且支持更多配置。