函数节流和防抖
窗口的resize、scroll、输入框内容校验等操作时,如果这些操作处理函数是较为复杂或页面频繁重渲染等操作时,在这种情况下如果事件触发的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少触发的频率,同时又不影响实际效果。
函数防抖(debounce)
当持续触发事件时,debounce 会合并事件且不会去触发事件,当一定时间内没有触发再这个事件时,才真正去触发事件,只会触发最后一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function debounce(callback, delay) { var timer = null, context, args; return function (e) { context = this; args = arguments; clearTimeout(timer); timer = setTimeout(() => { callback.apply(context, args) }, delay); } } window.onscroll = debounce(function (e) { console.log("触发滚动", e) }, 500)
|
立刻执行
不希望非要等到事件停止触发后才执行,希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
加个 immediate 参数判断是否是立刻执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function debounce(callback, delay, immediate) { var timer = null, context, args; return function (e) { context = this; args = arguments; clearTimeout(timer); if (immediate) { var didRun = !timer; if (didRun) { callback.apply(context, args); } } timer = setTimeout(() => { callback.apply(context, args) }, delay); } }
|
返回值
此时注意一点,就是绑定的函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再return 的时候,值将会一直是undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。
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(callback, delay, immediate) { var timer = null, context, args; var result; return function (e) { context = this; args = arguments; clearTimeout(timer); if (immediate) { var didRun = !timer; if (didRun) { result = callback.apply(context, args); } } timer = setTimeout(() => { callback.apply(context, args) }, delay); return result; } }
|
取消
最后我们再思考一个小需求,我希望能取消 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 25 26 27 28 29 30 31 32 33 34
| function debounce(callback, delay, immediate) { var timer = null, context, args; var result; var debounced = function (e) { context = this; args = arguments; clearTimeout(timer); if (immediate) { var didRun = !timer; if (didRun) { result = callback.apply(context, args); } } timer = setTimeout(() => { callback.apply(context, args) }, delay); return result; }
debounced.cancel = function () { clearTimeout(timeout); timeout = null; };
return debounced; }
|
函数节流
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
使用时间戳
使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function throttle(callback, delay) { var previous = 0; var context, args; return function () { context = this; args = arguments; var now = new Date().getTime(); if (now - previous > delay) { previous = now; callback.apply(context, args); } } }
|
使用定时器
接下来,我们讲讲第二种实现方式,使用定时器。
当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,重置定时器,这样就可以设置下个定时器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function throttle(callback, delay) { var timeout; var previous = 0;
return function () { context = this; args = arguments; if (!timeout) { timeout = setTimeout(function () { timeout = null; callback.apply(context, args) }, delay) } } }
|
比较使用时间戳,使用定时器两个方法:
第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
双剑合璧
想要控制第一次是否立刻执行,事件停止触发后是否会再执行一次事件。
那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
- leading:false 表示禁用第一次执行
- 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
| function throttle(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {};
var later = function () { previous = options.leading === false ? 0 : new Date().getTime(); timeout = null; func.apply(context, args); if (!timeout) context = args = null; };
var throttled = function () { var now = new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } }; return throttled; }
|
总结
- 函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。
- 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。
应用场景
- debounce
- search搜索,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
- throttle
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
参考
https://juejin.im/post/5b8de829f265da43623c4261
https://juejin.im/post/5b7b88d46fb9a019e9767405?utm_medium=fe&utm_source=weixinqun
https://github.com/mqyqingfeng/Blog/issues/22
https://github.com/mqyqingfeng/Blog/issues/26