防抖和节流都可以用来控制函数执行的频率。
防抖 (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 ...
|
实现
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 之内的调用都会被忽略,这就是节流的作用。