场景
1,有些用户很调皮,快速多次点击一个按钮。
2,用户不必特地捣乱,他在一个正常的操作中,都有可能在一个短的时间内触发非常多次事件绑定程序。比如页面绑定的 resize 事件。
分析
怎么解决?函数节流就是一种办法。函数节流,简单地讲,就是让一个函数无法在很短的时间间隔内连续调用,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。
函数节流的原理挺简单的,估计大家都想到了,那就是定时器。当我触发一个时间时,先 setTimout 让这个事件延迟一会再执行,如果在这个时间间隔内又触发了事件,那我们就 clear 掉原来的定时器,再 setTimeout 一个新的定时器延迟一会执行,就这样。
解决方法
在《JavaScript 高级程序设计》22.3.3节有介绍函数节流,里面封装了这样一个函数节流函数:
tools.throttle = function (method, context) { clearTimeout(method.tId); method.tId = setTimeout(function(){ method.call(context); },500); }
它把定时器 ID 存为函数的一个属性。而调用的时候就直接写
window.onresize = function(){ throttle(myFunc); }
这样两次函数调用之间至少间隔 100ms。
下面是一个优化版函数:
/** * * @param fn {Function} 实际要执行的函数 * @param delay {Number} 延迟时间,也就是阈值,单位是毫秒(ms) * * @return {Function} 返回一个“去弹跳”了的函数 */ function debounce(fn, delay) { // 定时器,用来 setTimeout var timer // 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数 return function () { // 保存函数调用时的上下文和参数,传递给 fn var context = this var args = arguments // 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn clearTimeout(timer) // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作), // 再过 delay 毫秒就执行 fn timer = setTimeout(function () { fn.apply(context, args) }, delay) } }
使用方法:
document.querySelector('button').addEventListener("click", debounce(function(e){ // 执行代码 },200));
Throttle 方案
上面的的方案被称为 Debounce,某些情况下对于 Debounce 的处理方式我们可能不满意,比如对每个 500ms 的间隔的事件的连续触发,我们想要 handler 至少执行一次,可能是在 500ms 的开头,也可能是在结尾,比如是开头,此时我们会怎么做?我们可能会想到每次触发事件时,把当次的触发时间和上次的「handler 的执行时间」(而 debounce 是上次事件的触发时间)对比,那么每次事件的触发时间和上次 handler 的执行时间会有个差值,如果这个差值大于 500ms,那么理所应当地,执行 handler 并记录此时的执行时间作为下一次触发时的参考时间;如果小于 500ms ,就什么也不做。这个延时到期了之后执行 handler,执行 handler 之后的再次触发事件时就创建一个新的时长为 500ms 的延迟。这样我们就保证了每个 500ms 内的多次事件触发的第一次总会得到处理。这种短时间间隔内处理多次事件触发的机制就是 Throttle。相同情形下,10s 中连续触发事件,任意相邻两次触发时间间隔小于 500ms,debounce 只会执行一次 handler 而 throttle 会执行 10(或者 11)次。
代码实现:
/** * * @param fn {Function} 实际要执行的函数 * @param delay {Number} 执行间隔,单位是毫秒(ms) * * @return {Function} 返回一个“节流”函数 */ function throttle(fn, threshhold) { // 记录上次执行的时间 var last // 定时器 var timer // 默认间隔为 250ms threshhold || (threshhold = 250) // 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数 return function () { // 保存函数调用时的上下文和参数,传递给 fn var context = this var args = arguments var now = +new Date() // 如果距离上次执行 fn 函数的时间小于 threshhold,那么就放弃 // 执行 fn,并重新计时 if (last && now < last + threshhold) { clearTimeout(timer) // 保证在当前时间区间结束后,再执行一次 fn timer = setTimeout(function () { last = now fn.apply(context, args) }, threshhold) // 在时间区间的最开始和到达指定间隔的时候执行一次 fn } else { last = now fn.apply(context, args) } } }
原理也不复杂,相比 debounce,无非是多了一个时间间隔的判断,其他的逻辑基本一致。throttle 的使用方式如下:
document.querySelector('button').addEventListener("click", throttle(function(e){ // 执行代码 },200));
参考:
http://www.alloyteam.com/2012/11/javascript-throttle/
https://blog.csdn.net/redtopic/article/details/69396722
https://www.jianshu.com/p/8b0757bdaeaf