在项目中,我们经常会绑定一些持续触发的事件,比如 resize、scroll、mousemove 等等,如果事件调用无限制,会加重浏览器负担,导致用户体验差,我们可以使用 debounce(防抖)和 throttle(节流)的方式来减少频繁的调用,同时也不会影响实际的效果。
防抖(debounce) 防抖,顾名思义,防止抖动,指在一段时间内,不论触发多少次,都已最后一次为准。 换句话说,在触发事件后 n 秒后才执行函数,如果在 n 秒内再次触发了事件,则会重新计算函数执行时间。 总而言之,就是不管触发多少次,都要最后一次事件后 n 秒内不再触发事件,才会执行。 那么防抖有哪些应用场景呢?
登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖;
调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
文本编辑器实时保存,当无任何更改操作一秒后进行保存
下面我们实现了一个简单的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function debounce (f, wait ) { var timer; return (...args ) => { clearTimeout (timer); timer = setTimeout (function ( ) { f(...args); }, wait); }; } var count = 1 ;var actionFun = () => { console .log(count++); }; window .addEventListener("resize" , debounce(actionFun, 1000 ));
从上面代码可以看出防抖重在清零 clearTimeout(timer) 接下来处理 this 的指向和 event 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function debounce (f, wait ) { var timer; return (...args ) => { var context = this ; clearTimeout (timer); timer = setTimeout (function ( ) { f.call(context, ...args); }, wait); }; } var count = 1 ;var actionFun = (e ) => { console .log(count++); console .log(this ); console .log(e); }; window .addEventListener("resize" , debounce(actionFun, 1000 ));
防抖函数可以分为立即执行和非立即执行版本:
立即执行函数:立即执行就是触发事件后马上先执行一次,之后在设定 wait 时间内触犯的事件无效,不会执行,直到用户停止执行事件等待 wait 秒后才可以重新触发执行
非立即执行函数: 多次触发事件,只会在最后一次触发事件后等待设定的 wait 时间结束时执行一次。 下面我们增加一个参数 immediate 来判断是否立即执行:
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 29 30 31 32 33 34 function debounce (f, wait, immediate = false ) { var timer; var dobounced = (...args ) => { var context = this ; if (timer) clearTimeout (timer); if (immediate) { var callNow = !timer; timer = setTimeout (function ( ) { timer = null ; }, wait); if (callNow) f.call(context, ...args); } else { timer = setTimeout (function ( ) { f.call(context, ...args); }, wait); } }; } var count = 1 ;var actionFun = (e ) => { console .log(count++); console .log(this ); console .log(e); }; window .addEventListener("resize" , debounce(actionFun, 1000 , true ));
如过希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function debounce (f, wait, immediate = false ) { var timer; var dobounced = (...args ) => { var context = this ; if (timer) clearTimeout (timer); if (immediate) { var callNow = !timer; timer = setTimeout (function ( ) { timer = null ; }, wait); if (callNow) f.call(context, ...args); } else { timer = setTimeout (function ( ) { f.call(context, ...args); }, wait); } }; dobounced.cancel = () => { clearTimout(timer); timer = null ; }; return dobounced; }
节流 (throttle) 节流,顾名思义,控制水的流量。控制事件发生的频率,如控制为 1s 发生一次,甚至 1 分钟发生一次。与服务端(server)及网关(gateway)控制的限流 (Rate Limit) 类似。 换句话说,连续触发事件但在 n 秒内只执行一次函数。 那么节流又有哪些应用场景呢?
scroll 事件,每隔一秒计算一次位置信息等
浏览器播放事件,每个一秒计算一次进度信息等
input 框实时搜索并发送请求展示下拉列表,没隔一秒发送一次请求 (也可做防抖)
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
使用时间戳 让我们来看第一种方法:使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function throttle (fn, wait ) { var prev = 0 ; return (...args ) => { let now = Date .now(); let context = this ; if (now - prev > wait) { fn.apply(context, args); prev = now; } }; } var count = 1 ;var actionFun = (e ) => { console .log(count++); console .log(this ); console .log(e); }; window .addEventListener("resize" , throttle(actionFun, 5000 ));
使用定时器 接下来,我们讲讲第二种实现方式: 使用定时器,当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行结束,然后执行函数,清空定时器,这样就可以设置下个定时器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function throttle (fn, wait ) { var timer; return (...args ) => { let context = this ; if (!timer) { timer = setTimeout (() => { timer = null ; fn.apply(context, args); }, wait); } }; } var count = 1 ;var actionFun = (e ) => { console .log(count++); console .log(this ); console .log(e); }; window .addEventListener("resize" , throttle(actionFun, 5000 ));
所以比较两个方法:
第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
节流优化 现在我们将上面两种方法合并,既可以立刻执行,又可以停止触发的时候还能再执行一次。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 function throttle (fn, wait ) { var timer, context, args, result; var prev = 0 ; var later = function ( ) { prev = Date .now(); timer = null ; fn.apply(context, args); }; var throttled = function ( ) { let now = Date .now(); var remaining = wait - (now - prev); context = this ; args = arguments ; if (remaining <= 0 || remaining > wait) { if (timer) { clearTimeout (timer); timer = null ; } prev = now; fn.apply(context, args); if (!timer) context = args = null ; } else if (!timer) { timer = setTimeout (later, remaining); } }; return throttled; } var count = 1 ;var actionFun = (e ) => { console .log(count++); console .log(this ); console .log(e); }; window .addEventListener("resize" , throttle(actionFun, 5000 ));
现在,我需要能够自定义是否立刻执行、和是否可以停止触发的时候还能再执行一次,现在增加第三个参数 options,然后根据传值判断如何执行:
leading: fasle,表示禁用立刻执行;
trailing: false,表示禁用停止触发的回调
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 function throttle (fn, wait, options ) { var timer, context, args, result; var prev = 0 ; if (!options) options = {}; var later = function ( ) { prev = options.leading === false ? 0 : Date .now(); timer = null ; fn.apply(context, args); }; var throttled = function ( ) { let now = Date .now(); if (!prev && options.leading === false ) prev = now; var remaining = wait - (now - prev); context = this ; args = arguments ; if (remaining <= 0 || remaining > wait) { if (timer) { clearTimeout (timer); timer = null ; } prev = now; fn.apply(context, args); if (!timer) context = args = null ; } else if (!timer && options.trailing !== false ) { timer = setTimeout (later, remaining); } }; return throttled; } var count = 1 ;var actionFun = (e ) => { console .log(count++); console .log(this ); console .log(e); }; window .addEventListener("resize" , throttle(actionFun, 5000 , { leading : false })); window .addEventListener("resize" , throttle(actionFun, 5000 , { trailing : false }));
注意,这里leading和trailing不能同时设为false。 同样取消也是增加一个cancel函数:
1 2 3 4 5 throttled.cancel = function ( ) { clearTimeout (timer); prev = 0 ; timer = null ; }
参考文献 https://github.com/mqyqingfeng/Blog/issues/26