Debounce和Throttle是两种常用的JS技巧,同时也是非常实用的技术。 所以有大量的文章专门介绍,包括不少洗稿了CSS-Trick那篇文章,尤其是那些用电梯做比喻的文章,不得不说,电梯的比喻烂透了。

简单来说,debounce在计算机词典中译作防反跳,其他常见翻译是防抖动,由词根 de-bounce 组成,具体详解见下文。throttle可译作节流,那些写成截流的,估计是用了拼音输入法并且没理解throttle含义,截流的英文通常译作closure,截流原指堵截水流,使改变方向或提高水位,所以叫截流根本是牛头不对马嘴;而节流本意是节制水流,比喻节约开支。其实想想成语开源节流或者节流阀门,就明白了。

这两个从字面来看,似乎能得到些感性上的大致理解。下面,细说一下这两个技巧。

Debounce

Debounce

电路中的bounce现象

如果学过数字电路/模拟电路的话,可能会知道,Bounce现象最开始是出现在一些电路设计中。机械开关或继电器闭合时,开关单元通常会发生反弹,这种抖动会导致传输的信号不清晰,会导致多次的状态转换。 就类似一个皮球从高出落下,要经历过几次反弹后才最终稳定下来一样,这也是bounce原来的语意。

比如键盘按下一个字母J,但是会得到了好几个J,或者鼠标单击变双击,这些都是常见的抖动现象。

为了解决bounce问题,电路设计中会引入debounce,即防反跳器,以消除这类有害的抖动。

在程序设计中,必然也会有debounce这样的设计。

总之,Debounce的概念是:一个函数被调用过一次后,在阈值时间内未被再次调用,那么当这段时间过去后,就执行最后那一次调用函数。

Throttle

何为 Throttle

从字面理解,就是类似一条水管上的阀门,当流量太大的时候,就需要扭紧阀门降低流量。函数节流后,代码就不会在没有间断过的情况下,不断调用执行,而是以一种被限流状的态执行。

转到前端的场景,比如在scroll事件中触发的函数,Throttle可以让函数调用阈值降到1S一次。那么在1S内,函数就不会被调用2次,10s内,无论有多少次scroll事件中触发,函数只调用10次。

总之,throttle方法规定了在给定时间内,函数的最大可执行次数。

Debounce vs Throttle

我们把100ms作为间隔的时间值,下面举例子说明两者区别:

Debounce

100ms已过,期间该函数没有被调用过,那么可执行该函数了。

一个函数在很短的时间间隔内被连续调用了1000次,这个过程用了2s,如果你对该函数做了一个100ms的debounce处理,那么可能在2.3s时,由于时这段爆发式调用已经结束并且间隔了100ms都没有新的调用进来,那么执行一次该函数。

Throttle

每100ms,最多只执行一次该函数。

10s内,你一共连续调用了1000次该函数,那么节流处理后,10s也就是10000ms时间内,该函数总共最多执行了

10000/100 = 100次。

实现Debounce方法

debounce方法接受两个参数:1、需要防抖动处理的函数。2、延迟时间。

 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
/**
*
*@param func {Function} 实际执行的函数
*@param delay {Number} 延迟阈值,毫秒单位
*@return {Function}
*/
function debounce(func,delay){
  // set a timer for setTimeout
  let timer
  //return the function that will be triggered after delay milliseconds
  return function(){
    // pass context and argument to func
    let context = this
    let args = arguments

    // each time this function is invoked,then clear timeout and don't fired func
    clearTimeout(timer)

    //The function will be called after it stops being called for
    // N milliseconds.
    timer = setTimeout(function(){
      func.apply(context,args)
    },delay)
  }
}

以上实现的完整Demo可戳这里。 codepen预览如下 ,看不到的请自备梯子。

可以实现一个ECMA2015+版本。会简洁不少。

debounce返回了一个箭头函数,箭头函数内部的this不会出现动态绑定的情况,省去了let context = this为了保存this不变而做的 hack。箭头函数利用...args作为「rest参数」,将arguments数组收集起来传入函数内,省去了 let args = arguments这一步操作,最后setTimeout函数内的func(...args)方法,替代原来的func.apply(..),可以认为是apply(..) 的缩写。

 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
/**
 *
 * @param func {Function} 要进行处理的函数
 * @param delay {Number} 延迟阈值,毫秒单位
 * @returns {Function}
 */
function debounce(func, delay) {
    // set a timer for setTimeout
    let timer

    //return a function that will be triggered after delay milliseconds
    return (...args) => {

        // each time this function is invoked,then clear timeout and don't fired func
        clearTimeout(timer)

        //The function will be called after it stops being called for
        // N milliseconds.

        timer = setTimeout(() => {
            timer = null
            func(...args)
        }, delay)
    }
}

完整demo见 这里

实现Throttle方法

类似debounce,在throttle中,接收两个参数,实际执行的函数 func 和阀门参数threshold来做限流。

 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
/**
 *
 * @param func
 * @param threshold
 * @return function
 */
function throttle(func, threshold) {
  let timer 
  let last
  threshold || (threshold = 16)
  return function(){
    let context = this
    let args = arguments
    let now = +new Date()
    if(last && now< last+threshold){
      clearTimeout(timer)
      timer = setTimeout(function(){
        last = now
        func.apply(context,args)
      },threshold)
    }else{
      last = now
      func.apply(context,args)
    }
  }
}

完整demo

ES6实现版本:

 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
/**
 *
 * @param func
 * @param threshold
 * @return function
 */
function throttle(func, threshold = 16) {
    let last

    let timer

    return (...args) =>{

        let now = +new Date()

        if (last && now < last + threshold) {
            clearTimeout(timer)

            timer = setTimeout(function () {
                last = now
                func(...args)
            },threshold)
        } else {
            last = now
            func(...args)
        }
    }

}

完整demo