前言
- 在开发中我们经常会遇到事件频繁回调导致的性能和体验下降的问题,比如滚动事件回调或者输入框根据用户输入实时返回sug的场景。
- 一方面用户并不需要那么精确的实时,另一方面实时的操作会发送不必要的多余请求,给服务端造成压力,也给端上造成困扰。
throttle和debounce就是为了解决这类问题出现的
节流 throttle
限制函数的执行频率,保证一定时间内只执行一次函数调用。无论触发频率多高,都会在指定时间间隔内执行一次函数。
节流有时间戳实现和定时器实现两种
时间戳实现
- 在首次触发事件时,函数会立即执行并设置当前时间戳作为基准
- 在后续 持续触发事件 过程中,只有事件的时间戳和基准时间戳之差大于 隔断时间 才会执行函数并把当前时间戳作为基准,之后继续判断后续事件 重复第二步
定时器实现
- 在 持续触发事件 的过程中,函数不会立即执行但会设置一个定时器,定时器结束时 执行函数 并重置定时器
- 后续事件中 只有定时器不存在时 才执行第一步
举个例子
假设我们在排队进电影院,进入电影院的规则是 每10分钟进一波人
第一个人2:00来了,他需要2:10才能进,第二个人2:02来了,他也需要2:10才能进,第三个人2:09来了,他也需要2:10才能进。第四个人2:11来了,他需要在2:20才能进
应用场景
用于处理 连续触发 的事件,比如滚动事件、鼠标移动事件等。控制函数的执行频率,以减少资源消耗和提高性能。
比如
- 实现DOM元素的拖放功能
mousemove - 搜索关联
keyup - 计算鼠标移动距离
mousemove - 画布模拟草图功能
mousemove - 射击游戏中的
mousedown/keydown事件(每单位时间只能发射一颗子弹) - 监视滚动
scroll事件(添加节流后,只要滚动页面,就会每隔一段时间才会计算)
实现示例
// delay表示延迟的时间间隔(单位毫秒),callback是需要进行防抖的函数
function throttle(delay, callback) {
let timeoutID;
let lastExec = 0;
function wrapper() {
// 通过 `self` 变量临时保存 `this` 的值,从而在 `exec` 函数中通过 `callback.apply(self, args)` 传入正确的 `this` 值
const self = this;
// 计算距离最近一次函数执行后经过的时间 `elapsed`
const elapsed = Number(new Date()) - lastExec;
const args = arguments;
function exec() {
// 更新最近一次函数的执行时间
lastExec = Number(new Date());
callback.apply(self, args);
}
// 并清除之前设置的计时器
clearTimeout(timeoutID);
// 如果经过的时间大于设置的时间间隔 `delay`,那么立即执行函数
if (elapsed > delay) {
exec();
} else {
// 如果经过的时间小于设置的时间间隔 `delay`,那么通过 `setTimeout` 设置一个计数器,让函数在 `delay - elapsed` 时间后执行
timeoutID = setTimeout(exec, delay - elapsed);
}
}
return wrapper;
}
const self = this; 和 const args = arguments; 这两句代码是非常有必要的
我们知道在JS非箭头函数中的this并不是定义时的上下文,而是执行时的上下文,而我们的debounce把原来的函数做了一层包装会导致执行时this的改变。这里的两行代码就巧妙的解决了这个问题
使用示例
// 实际做事的函数
function foo() { console.log('foo..'); }
// 对foo函数进行节流,返回新的函数
const fooWrapper = throttle(200, foo);
for (let i = 1; i < 10; i++) {
// 使用节流函数,限制每200ms执行一次
setTimeout(fooWrapper, i * 30);
}
// => foo 执行了三次
// => foo..
// => foo..
// => foo..
防抖 debounce
在事件被触发n秒后去执行回调函数。如果n秒内该事件被重新触发则重新计时。结果就是将频繁触发的事件合并为一次,且在最后执行
防抖又细分为头执行和尾执行
头执行
触发事件后立即执行函数,然后n秒内没有事件再触发才会继续执行函数
尾执行
当持续触发事件时,debounce 会 合并事件且不会去执行函数,当一定时间内没有再触发这个事件时,才真正去执行函数
举个例子
假设我们在排队进电影院,进入电影院的规则是 最后一个人来了之后等10分钟没有人再来才可以进去,否则大家一起等。
第一个人来了,他需要等10分钟看有没有其他人来,如果10分钟内第二个人来了,那么第一个人和第二个人需要重新开始等10分钟看有没有其他人来,同样的道理,如果10分钟内第三个人来了,那么第一个人、第二个人、第三个人需要重新开始等10分钟看有没有其他人来,依次类推。假设第5个人来了,这五个人等了10分钟后没有人再来,他们就可以进影院了
应用场景
用于处理 频繁触发 的事件,比如窗口大小调整、搜索框输入等。避免函数执行过多次,以减少网络开销和性能负担
比如
- 每个调整大小/滚动都会触发统计事件
- 验证文本输入(在连续文本输入后,发送
Ajax请求进行验证) - 监视滚动
scroll事件(在添加去抖动后滚动,只有在用户停止滚动后才会确定它是否已到达页面底部)
注意事项
我们从debounce的原理可以知道,如果时间间隔很长事件可能永远不会触发。所以我们使用的时候这个时间设置的不宜太长
实现示例
// `debounce` 函数可以借助 `throttle` 函数实现
function debounce(delay, callback) {
// 保存超时id
let timeoutID;
function wrapper() {
// 通过 `self` 变量临时保存 `this` 的值,从而在 `exec` 函数中通过 `callback.apply(self, args)` 传入正确的 `this` 值
const self = this;
const args = arguments;
function exec() {
callback.apply(self, args);
}
// 每次清空定时器,并重新设置定时器
// 这里就做到了在规定时间内多次函数调用把前一个取消的效果
clearTimeout(timeoutID);
timeoutID = setTimeout(exec, delay);
}
return wrapper;
}
使用示例
function bar() { console.log('bar..'); }
const barWrapper = debounce(200, bar);
for (let i = 1; i < 10; i++) {
setTimeout(barWrapper, i * 30);
}
// => bar 执行了一次
// => bar..
结合装饰器
装饰器本质上也是一个函数,它可以让其它函数或者类在不做代码修改的情况下添加额外的功能,装饰器的返回值也是函数/类。
我们可以使用装饰器装饰我们实际的函数,将它转变成一个debounce或者throttle
示例实现
/**
* 装饰器的debounce
* @param delay
*/
export function debounce(delay: number): Function {
return (
target: Object,
propertyKey: string,
propertyDesciptor: PropertyDescriptor
) => {
const method = propertyDesciptor.value;
let timer = null;
propertyDesciptor.value = (...args) => {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => method(...args), delay);
};
return propertyDesciptor;
};
}
参考资料
- JavaScript 高级系列之节流 [throttle] 与防抖 [debounce]
- 防抖(Debounce) & 节流(Throttle)
- 每天阅读一个 npm 模块(4)- throttle-debounce
- 前端性能优化的利器-JS中的防抖和节流
- 说说 JavaScript 中函数的防抖 (Debounce) 与节流 (Throttle)
- 浅谈Debounce 与 Throttle
- [JS性能优化]函数去抖(debounce)与函数节流(throttle)
- lodash中的throttle
- lodash中的debounce
- throttle-debounce库
- 函数防抖与节流和TypeScript装饰结合
- 实现一个debounce的装饰器
- 「JS篇」这篇文章助你理解函数防抖与函数节流
- 防抖和节流原理分析