大家好,我是前端西瓜哥,今天带大家来手写(throttle)节流函数。
在这之前,我们简单了解下节流函数是什么。
节流函数是什么?
节流函数,就是降低一个函数的执行频率。每隔一段时间,才执行一次函数。
假设我们 1s 中执行了 8 次函数:
1 2 3 4 5 6 78
---------------------
添加节流能力后,我们让函数只在特定的时间间隔执行,且其中的一部分函数并没有被真正调用:
// 节流后
1 3 5 6 8
---------------------
节流函数的常见使用场景
使用节流的场景通常为一些会高频触发的事件,包括滚动、改变窗口大小、输入内容、光标移动事件等。
它们的触发频率非常高,不进行限制的话很容易产生一些性能问题。
下面是一些常见的使用节流函数的场景:
-
搜索框根据用户输入的内容,进行智能补全提示。需要通过节流来减少对服务器的压力
-
网页底部滚动加载。如果不使用节流,用户滚动到底部时发送第一个请求时,数据还没返回,就会持续触发滚动到底部的事件,导致同一时间发送大量请求。我们可以考虑加上节流,这个时间间隔也记得调大一点,处理弱网环境。
-
通过 JS 监听窗口大小改变 resize 事件,改变某个元素的宽度,可以通过节流来减少浏览器的重绘操作。
实现
我们需要实现一个 throttle 函数,这个函数接受原函数 fn 和时间间隔 wait,然后返回一个支持节流的新函数。
用法如下:
const printNum = (num) => {
console.log(num);
}
// 设置
const throttled = throttle(printNum, 300);
throttled(0);
setTimeout(() => { throttled(1); }, 100);
setTimeout(() => { throttled(2); }, 200);
setTimeout(() => { throttled(3); }, 250);
// 0
// 3 (300ms+ 左右后输出)
实现上,只要做到将高频的函数连续调用,变成均匀且低频的调用,就算是节流函数了。
实现的核心在于 利用时间戳控制好定时器的调用时机。
我们先看代码实现:
function throttle(fn, wait = 0) {
let timerId;
let lastInvoke = Number.MIN_SAFE_INTEGER; // 上次调用时间
return function(...args) {
// 当前时间
const currTime = new Date().getTime();
// 距离下次执行的剩余时间
const remain = Math.max(lastInvoke + wait - currTime, 0);
// 更新定时器,确保同一时间只有一个定时器在运行
clearTimeout(timerId);
timerId = setTimeout(() => {
lastInvoke = new Date().getTime();
fn(...args);
}, remain);
}
}
// 使用方式
const throttled = throttle(printNum, 300);
throttle(0);
演示 demo:
https://codepen.io/F-star/pen/GRxvRRd?editors=0010
首先我们需要通过闭包保存一些私有变量:
-
原函数最后一次被调用的时间戳 lastInvoke;
-
定时器 id,当函数被调用时,清除原来的定时器,再执行一个新的定时器,确保任何时刻只有一个定时器在运行;
-
要被调用的原函数 fn;
-
调用的时间间隔 wait
当调用 throttle 返回的增强的函数时,
先计算调用下一个函数的剩余时间 remain:首先通过 lastInvoke + wait
计算出下一次应该执行的时间戳,然后用这个时间戳减去当前时间戳 currTime,就能得到剩余时间了。
剩余时间可能是负数,比如我们调用 throttled 后过了很长时间再次执行的场景。这种情况下我们就将其设置为 0,接着将这个剩余时间传到 setTimeout 里执行。
定时器执行时,我们在执行原函数前,先更新 lastInvoke。如果放后面,可能会因为原函数执行报错,导致 lastInvoke 更新失败。
以上代码的实现其实是比较粗糙的,有一些 case 没能处理到。
如果你想要实现一个比较完美节流函数,可以参考 lodash.throttle 的实现,它考虑了更多的边界情况,并提供了一些额外功能,代码实现也较为复杂。
lodash.throttle 考虑了以下细节:
-
第一次执行时,使用同步方式执行;
-
支持中途取消执行(cancel);
-
支持中途立即执行(flush);
-
返回上一次原函数执行时的返回值;
-
可以选择是否执行一轮的 leading 和 trailing 边界情况;
-
…
结尾
实现一个简单的节流函数,关键在于维护好最后一次调用原函数的时间戳,通过它来计算下一次执行时机,并使用定时器来执行。
一个比较完善的节流函数的实现并不简单,需要考虑一些边界情况,我更推荐你使用知名 lodash 工具库提供的 throttle 方法。
我是前端西瓜哥,欢迎关注我,学习更多前端面试题。