关于防抖和节流,是一个老生常提的话题了,随便在网上搜一搜,都可以找到现成可以使用的函数,但是现成的函数的功能实现的都是已经比较完善的了,我们并不清楚它如何或者为什么要这样设计,所以我们今天就从头开始的来深入的了解一下这两个方法,主要参考的是 Lodash 当中的 _.throttle
和 _.debounce
两个方法
什么是防抖和节流
我们可以使用一个现实中常见的例子来进行举例,比如使用电梯运送策略来说明这两个方法,比如每天上班大厦底下的电梯,把电梯完成一次运送,类比为一次函数的执行和响应,假设电梯有两种运行策略 throttle
和 debounce
,超时设定为 15
秒,不考虑容量限制
throttle
策略的电梯,保证如果电梯第一个人进来后,15
秒后准时运送一次,不等待,如果没有人,则待机debounce
策略的电梯,如果电梯里有人进来,等待15
秒,如果有人进来,15
秒等待重新计时,直到15
秒超时,开始运送
但是有一点需要注意,当然你也可以采用箭头函数等一些
ES6
当中的方式来进行实现,但是本文大部分都是采用ES5
的方式来进行介绍,主要是以介绍原理为主
我们先从防抖开始看起
debounce
防抖即 debounce
,根据我们之前的思路,我们可以迅速的实现我们的第一版代码
1 | // 第一版 |
运行以后可以发现,是存在问题的,函数当中的 this
是指向 window
对象的,所以我们需要将 this
指向正确的对象,所以针对于这种情况,我们可以使用一个变量来保存当前的 this
注意,这里如果使用了箭头函数则直接传递
this
即可,因为箭头函数会从自己的作用域链的上一层继承this
1 | // 第二版 |
但是现在依然是存在一定问题的,因为我们传入的 fn
是有可能携带参数的,所以我们需要将其补上,也就有了下面的第三版
1 | // 第三版 |
目前为止,我们已经完成了一个较为完善的方法,但是现在的需求有所变化,我们不希望非要等到事件停止触发后才执行我们的函数,而是希望可以在开始的时候立刻执行函数,然后等到停止触发 wait
秒后,才可以重新触发执行,所以针对这种情况,我们添加一个 immediate
参数判断是否是立刻执行,如果 immediate
参数传递为 true
,则每隔 wait
秒后才会再次执行
1 | // 第四版 |
关于文中标注的两点需要注意的地方,第一点
timer
是闭包变量,初始化时是undefined
,setTimeout
返回的是定时器的id
,一个大于0
的数字,而clearTimeout
不会改变timer
的值- 若
timer
经历过赋值,即执行过setTimeout
,则!timer
为假,所以也就不再执行
关于第二点,在官方源码当中有提到,主要作用就是防止重复点击用的
Pass true for the immediate argument to cause debounce to trigger the function on the leading instead of the trailing edge of the wait interval. Useful in circumstances like preventing accidental double-clicks on a “submit” button from firing a second time.
我们在之前考虑到了我们传入的函数 fn
是可能带有参数的,但是同样的,它也有可能会有返回值的,但是当 immediate
为 false
的时候,因为使用了 setTimeout
,我们将 fn.apply(that, arguments)
的返回值 return
的时候,值将会一直是 undefined
,所以我们只在 immediate
为 true
的时候返回函数的执行结果,所以直接定义一个 result
进行接收以后在返回即可
1 | // 第五版 |
最后,我们再来考虑一点小需求,即我们希望可以取消 debounce
函数,比如我们设定了 debounce
的时间间隔是 10
秒钟,immediate
为 true
,这样一来,只有等待 10
秒后才能重新触发事件,所以我们希望有一个按钮,点击后可以取消防抖,这样再去触发,就可以又立刻执行,所以我们可以使用一个函数将 return
的部分包裹起来,然后给它添加一个取消的句柄即可
1 | // 第六版 |
可以使用下面的代码来进行测试
1 | var mousemove = debounce(function (e) { |
至此我们就已经完整实现了一个 debounce
函数,防抖看完之后,我们再来看看节流
throttle
同样的逻辑,我们这次采用 throttle
策略来进行实现,基本逻辑如下,我们使用时间戳来进行记录,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳,如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于就不执行
1 | // 第一版 |
上面是使用记录时间戳的方式来实现的,下面我们就来看看使用定时器的版本,当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器
1 | // 第二版 |
可以发现,虽然在实现上很类似我们上面第三版的 debounce
,但是它们的原理是有所不同的,先抛开这些,我们来对比一下上面两种实现方式
- 第一种事件会立刻执行,第二种事件会在
n
秒后第一次执行 - 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
所以我们可以将两者进行结合,即在一开始的时候可以立刻执行,然后在停止触发的时候还能再执行一次
1 | function throttle(fn, wait) { |
可以使用下面这段代码来进行测试
1 | var mousemove = throttle(function (e) { |
可以发现在鼠标刚移入的时候就会立即执行,并且在鼠标停止移动以后会在执行一次
总结
总结一下大致的使用场景
debounce
,特点是它在用户不触发事件的时候,才触发动作,并且抑制了本来在事件中要执行的动作- 如果不添加
immediate
参数,一开始是不会立即运行的,一般用于比如input
输入框的格式验或者提交按钮的点击事件等
- 如果不添加
throttle
,类似于水坝,不能让水流动不了,只能让水流慢些,换言之就是不能让用户的方法都不执行,它会强制函数以固定的速率执行- 一般用在比
input
,keyup
更频繁触发的事件中,如resize
,touchmove
,mousemove
,scroll
,另外还有动画相关的场景
- 一般用在比
如果不考虑额外功能,使用下面两种方式即可
1 | function debounce(fn, wait) { |