防抖与节流

防抖与节流

防抖和节流都可以用来控制函数执行的频率。

防抖 (debounce)

效果

  • 给定一个时间间隔,比如 200ms
  • 在第一次调用函数时,并不立即执行函数,而是延迟 200ms 再看情况执行
  • 如果在这 200ms 延迟内,函数又被调用了,取消之前的定时器,继续延迟 200ms 毫秒,直到一次 200ms 延迟正常结束且这段延迟内函数没有被调用,这时才真正执行这个函数。

例子

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
const debounce = require('lodash/debounce');

(async () => {
const sayHello = debounce(() => {
console.log(new Date(), 'Hello World');
}, 2000);

for await (_ of Array(10).fill()) {
await new Promise(resume => setTimeout(resume, 1000));
console.log(new Date());
sayHello();
}
})();

// 运行结果:
2021-01-19T01:23:42.802Z
2021-01-19T01:23:43.814Z
2021-01-19T01:23:44.815Z
2021-01-19T01:23:45.819Z
2021-01-19T01:23:46.820Z
2021-01-19T01:23:47.823Z
2021-01-19T01:23:48.825Z
2021-01-19T01:23:49.828Z
2021-01-19T01:23:50.829Z
2021-01-19T01:23:51.829Z
2021-01-19T01:23:53.831Z Hello World

因为这里设置防抖的间隔是 2000ms,而调用 sayHello 是每隔一秒调用一次,所以每当延时一秒的时候,函数又被调用,重新设置了一个新的 2000ms 延时,直到第 10 次调用结束后,这次延时 2000 ms 的间隔内不再有新的函数调用,等延时结束后才真正执行我们所需要防抖的函数:

1
2
3
() => {
console.log(new Date(), 'Hello World');
}

举个极端点的例子,如果在程序运行时,始终保持 1000 ms 的间隔调用函数,而防抖的间隔设置为 2000 ms,那么被防抖的函数永远也不会被执行,因为永远满足不了延时 2000 ms 的时间间隔内这个函数没有被调用的条件。我们把上面那个例子改写一下:

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
const debounce = require('lodash/debounce');

const sayHello = debounce(() => {
console.log(new Date(), 'Hello World');
}, 2000);

setInterval(async () => {
await new Promise(resume => setTimeout(resume, 1000));
console.log(new Date());
sayHello();
}, 1000);

// 运行结果:
2021-01-19T01:36:35.478Z
2021-01-19T01:36:36.490Z
2021-01-19T01:36:37.492Z
2021-01-19T01:36:38.494Z
2021-01-19T01:36:39.496Z
2021-01-19T01:36:40.498Z
2021-01-19T01:36:41.498Z
2021-01-19T01:36:42.500Z
2021-01-19T01:36:43.503Z
2021-01-19T01:36:44.503Z
2021-01-19T01:36:45.506Z
2021-01-19T01:36:46.508Z
... // 永远也不会打印 Hello World

实现

1
2
3
4
5
6
7
8
9
10
const debounce = (fn, interval) => {
let timer = null;

return () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(fn, interval);
};
};

毫无疑问,debounce 是一个高阶函数,调用它的返回结果是一个函数。在 debounce 中我们还需要一个闭包变量来 timer 来记录定时器,返回的函数是对被防抖函数的封装,主要思想是每次调用函数时,清除之前的定时器(如果有的话),重新设置一个新的定时器,这样就实现了一个简单的防抖函数。

节流 (throttle)

效果

  • 给定一个时间间隔,比如 200ms
  • 在每个 200ms 节流间隔内,哪怕函数被重复调用多次,也只执行函数一次

例子

改写一下上面那个例子

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
const throttle = require('lodash/throttle');

(async () => {
const sayHello = throttle(() => {
console.log(new Date(), 'Hello World');
}, 5000);

for await (_ of Array(10).fill()) {
console.log(new Date());
sayHello();
await new Promise(resume => setTimeout(resume, 1000));
}
})();

// 执行结果:
2021-01-19T02:18:46.864Z
2021-01-19T02:18:46.872Z Hello World
2021-01-19T02:18:47.878Z
2021-01-19T02:18:48.879Z
2021-01-19T02:18:49.880Z
2021-01-19T02:18:50.882Z
2021-01-19T02:18:51.875Z Hello World
2021-01-19T02:18:51.884Z
2021-01-19T02:18:52.887Z
2021-01-19T02:18:53.890Z
2021-01-19T02:18:54.893Z
2021-01-19T02:18:55.896Z
2021-01-19T02:18:56.886Z Hello World

这里我们设置了节流的间隔为 5000ms ,函数调用的间隔仍然是 1000ms。注意到在 18:46:864 ~ 18:51:864 这段时间间隔内函数被调用了5次,但实际只被执行了1次,这就是节流的作用。在一个节流间隔内,无论调用函数多少次,在这个间隔内只真正执行函数一次。

实现

节流函数的实现有很多种,这里用时间戳的方式实现。

1
2
3
4
5
6
7
8
9
10
const throttle = (fn, interval) => {
let timestamp = 0;

return () => {
if (Date.now() - timestamp > interval) {
timestamp = Date.now();
return fn();
}
};
}

即每次成功执行一次函数后,接下来 5000ms 之内的调用都会被忽略,这就是节流的作用。