引入
上篇我们讲了防抖
,这篇我们就谈谈防抖
的好兄弟 – 节流
。这里在老生常谈般的提一下他们两者之间的区别,顺带给读者巩固下。
PS: 开源节流
中节流与这个技术上的节流
,个人认为本质上是一样的。
区别
节流
: N
秒内只运行一次,若在N
秒内重复触发,只有第一次生效防抖
: N
秒后在执行该事件,若在N
秒内被重复触发,则重新计时 不过我认为还是防抖那篇文章有个读者的评论更显生动 ?, 在此对该读者表示感谢?。
节流
: 可以看做攻击间隔,点的再快没打出来也不会同时攻击两次。防抖
: 可以理解为回城,每点一下就要重新跑. 节流例子
这里我举两个常见的?,大家有什么更好的?可以在评论里回复
这个王者荣耀的攻击例子也算是节流,不过这个我就不举了。?
生活例子
这里跟大家举一个生活例子,更显生动。
我们知道一般女神回复舔?的概率都是比较低的,假设女神每天回复舔?一次。也就是说如果今天女生已经回复过了,那么无论舔?发再多的信息对方也是不会回的。今天的次数已经消耗完毕了,也只能等明天才能刷新。
短信验证
我们知道短信验证是在生活中很常见,其实短信验证便用到了节流的技术。
因为发短信其实是要钱的,为了避免一个用户重复点击导致发出多条短信就要使用节流。比如获取一次验证码的有效时间为60
秒,则60
秒内这个发送短信的方法不能再次触发。
手撕代码
xdm,接下来开始手撕代码了。这里有两种实现方式,分别是时间戳
实现和定时器
实现。
时间戳实现
原理
每次事件触发都会检查距离上次执行的时间间隔,如果超过指定的等待时间delay
,则执行函数。并且更新上一次执行时间为为当前的时间戳。
代码
function throttleByTimestamp(fn, wait) { let lastTime = 0; // 用于保存上一次执行的时间戳 return function () { const currentTime = new Date().getTime(); // 判断当前时间与上次执行时间差是否大于等待时间 if (currentTime - lastTime > wait) { // 如果满足条件,则执行原函数,并传递参数 fn.apply(this, arguments); // 更新上一次执行的时间戳为当前时间 lastTime = currentTime; } };}// 使用const throttledFn = throttleByTimestamp(function () { console.log("节流函数被执行");}, 500); // 每隔500毫秒执行一次// 等待时间const waitTime = async (time) => { return new Promise((resolve) => { setTimeout(() => { resolve(); }, time); });};const main = async() => { throttledFn(); // 换成 < 500ms的则只会执行一次 await waitTime(600); throttledFn();}main()/** * 输出: * 节流函数被执行 * 节流函数被执行 */
定时器实现
原理
我们在事件触发时设置一个定时器。
首次事件触发时,我们设置一个定时器,在等待一段时间后执行函数。当定时器触发前,如果有新的事件触发,我们会检查是否存在定时器,如果存在则跳过。当定时器触发时,会清除当前定时器,确保下一次事件能重新触发。代码
注: 测试例子跟上面的一样,此处不在贴啦。
function throttleByTimer(fn, delay) { let timer; return function () { const context = this; const args = arguments; if (!timer) { timer = setTimeout(() => { fn.apply(context, args); clearTimeout(timer); timer = null; }, delay); } };}
时间戳+定时器实现
我们回顾这两个实现方式,大家有没有发现这两个实现方式的区别?
时间戳
: 因为是拿当前时间减上一次触发的时间,所以一旦满足该条件,事件会立即执行。定时器
: 由于fn.apply
的函数写在了setTimeout
里,所以触发了,也得等delay
后才能执行,于是就有了这种事件停止触发后依然会再一次执行的效果。 如果我们要实现这样的一个需求,完成一个事件触发时立即执行,触发完毕还能执行一次的节流函数该如何做呢?
原理
此处借鉴掘金网友《6个瑞士卷》的分析,个人觉得逻辑写的很好。于是摘录了下来。
需要在每个delay
时间中一定会执行一次函数,因此在节流函数内部使用开始时间、当前时间与delay
来计算remaining
当remaining <= 0
时表示该执行函数了,如果还没到时间的话就设定在remaining
时间后再触发。当然在remaining
这段时间中如果又一次发生事件,那么会取消当前的计时器,并重新计算一个remaining
来判断当前状态。 代码
function throttle(fn, delay) { let timer; let lastTime = 0; return function () { let currentTime = Date.now(); // 计算距离上次执行fn到现在过去了多少时间,与delay做比较 let remaining = delay - (currentTime - lastTime); const context = this; const args = arguments; // 如果在remaining这段时间在发生,会取消当前的计时器 clearTimeout(timer); // 当remaining <= 0时表示该执行函数了 if (remaining <= 0) { fn.apply(context, args); lastTime = Date.now(); } else { // 如果还没到时间的话就设定在remaining时间后再触发 timer = setTimeout(fn, remaining); } };}