什么是防抖?
在前端开发中,我们常常会遇到一些频繁的事件触发,比如:
- window 的 resize、scroll
- mousedown、mouseover,mousemove
- keyup、keydown
这些事件触发是很频繁的,
1 | var count = 1; |
如上代码,响应用户鼠标移动的操作,鼠标从界面左边移动到右边,
一次移动就触发了这么多次操作,这个js代码只是简单地修改html内容,如果我们鼠标移动的相应函数要执行很复杂的DOM操作,或者要跟后端请求一些数据的话,假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。
解决这个问题有两种方式:
- debounce——防抖
- throttle——节流
防抖
防抖的原理就是,我一定要等待一个固定的时间段之后再执行回调函数,如果在我等待的这段时间里又一次被调用到了,那么我的计时器会清零,从此刻开始再等一个固定时间段之后再执行回调函数
自己实现版本
1 | function debounce(fn, timeout) { |
lodash 的 debounce 实现
- func
- wait
- options
- trailing:boolean 在wait的结尾调用func
- leading:boolean 在wait的开始调用func
1 | function debounce(func, wait, options) { |
这里我用文字来简单描述一下流程:
首次进入函数时因为 lastCallTime === undefined 并且 timerId === undefined,所以会执行 leadingEdge,如果此时 leading 为 true 的话,就会执行 func。同时,这里会设置一个定时器,在等待 wait(s) 后会执行 timerExpired,timerExpired 的主要作用就是触发 trailing。
如果在还未到 wait 的时候就再次调用了函数的话,会更新 lastCallTime,并且因为此时 isInvoking 不满足条件,所以这次什么也不会执行。
时间到达 wait 时,就会执行我们一开始设定的定时器timerExpired,此时因为time-lastCallTime < wait,所以不会执行 trailingEdge。
这时又会新增一个定时器,下一次执行的时间是 remainingWait,这里会根据是否有 maxwait 来作区分:
- 如果没有 maxwait,定时器的时间是 wait - timeSinceLastCall,保证下一次 trailing 的执行。
- 如果有 maxing,会比较出下一次 maxing 和下一次 trailing 的最小值,作为下一次函数要执行的时间。
最后,如果不再有函数调用,就会在定时器结束时执行 trailingEdge。
节流
节流则不会管用户上次是什么时候调用的,只要时间到达节流设置的时间段,就会调用回调函数
在 wait 开始时间段调用
1 | function throttole(func, wait) { |
在wait 结束时调用:
1 | function throttole(func, wait) { |
对比可以发现,
- 第一种方法在 timer 开始时就会触发回调函数,而第二种在 n 秒之后才会第一次执行
- 第一种方法停止触发后没有办法再次触发事件,而第二种在停止触发之后还会进行一次事件执行
如何自己添加参数来控制事件触发的时机
options:
- Leading:是否在timer 开始时触发函数
- trailing:是否在 timer 结束之后再触发一次函数
1 | function throttle(func, wait, options) { |
注意点:
- 分支分割条件并不严格二分,trailing 和 leading 类型都会进入第一个判断分支进行执行,只有 trailing 第一次进入时 或者 trailing 回调函数之前刚刚被调用过一次,在第二个分支来设置 计时器,就算这样,也只有在 trailing 情况下的最后一次触发,会调用到 setTimeout 里的 later 函数。
- 要在later 函数里清空previous,保证throttle函数时隔 大于 wait 的时间之后再次被触发