防抖和节流都可以用来控制函数执行的频率。
防抖 (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 -19 T01:23 :42.802 Z2021 -01 -19 T01:23 :43.814 Z2021 -01 -19 T01:23 :44.815 Z2021 -01 -19 T01:23 :45.819 Z2021 -01 -19 T01:23 :46.820 Z2021 -01 -19 T01:23 :47.823 Z2021 -01 -19 T01:23 :48.825 Z2021 -01 -19 T01:23 :49.828 Z2021 -01 -19 T01:23 :50.829 Z2021 -01 -19 T01:23 :51.829 Z2021 -01 -19 T01:23 :53.831 Z 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 -19 T01:36 :35.478 Z2021 -01 -19 T01:36 :36.490 Z2021 -01 -19 T01:36 :37.492 Z2021 -01 -19 T01:36 :38.494 Z2021 -01 -19 T01:36 :39.496 Z2021 -01 -19 T01:36 :40.498 Z2021 -01 -19 T01:36 :41.498 Z2021 -01 -19 T01:36 :42.500 Z2021 -01 -19 T01:36 :43.503 Z2021 -01 -19 T01:36 :44.503 Z2021 -01 -19 T01:36 :45.506 Z2021 -01 -19 T01:36 :46.508 Z...
实现
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 -19 T02:18 :46.864 Z2021 -01 -19 T02:18 :46.872 Z Hello World2021 -01 -19 T02:18 :47.878 Z2021 -01 -19 T02:18 :48.879 Z2021 -01 -19 T02:18 :49.880 Z2021 -01 -19 T02:18 :50.882 Z2021 -01 -19 T02:18 :51.875 Z Hello World2021 -01 -19 T02:18 :51.884 Z2021 -01 -19 T02:18 :52.887 Z2021 -01 -19 T02:18 :53.890 Z2021 -01 -19 T02:18 :54.893 Z2021 -01 -19 T02:18 :55.896 Z2021 -01 -19 T02:18 :56.886 Z 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 之内的调用都会被忽略,这就是节流的作用。