防抖(debounce)和节流(throttle)两词其实并非计算机领域的原生词语。追根溯源,防抖一词来自于弱电领域,指代的是消除外界对于开关扰动的技术。而节流来自于流体力学,指代的是限定流体流量的一种技术。由于JavaScript拥有事件驱动的特性,为避免事件频繁触发导致性能的损耗,防抖和节流这两种技术在JavaScript中亦被广泛应用。

为什么需要防抖和节流?

日常与浏览器打交道比较多的前端开发者对于浏览器的各种事件想必都不会陌生,我们会针对某一个特定的事件绑定对应的响应函数,在事件被触发时让浏览器自动调用该函数。

1
2
3
4
5
function onResizeHandler() {
    // do something on resize
}

window.addEventListener('resize', onResizeHandler);

如果只是一些触发频率很低的事件,那么上面的代码并没有什么问题。但是如果像resize这样可能在短时间内被频繁触发的事件(比如click/keydown/touchStart等),我们不去做任何的处理的话,可能导致事件的响应函数在短时间内被大量的触发,造成浏览器卡顿,伤害用户体验。

而节流和防抖一个很经典的应用场景就是去控制我们的事件的触发,节省浏览器开销。

什么是防抖

JavaScript中的防抖指的是只有在x ms内没有调用该函数,才会真正调用该函数。如下图

该图上方是事件触发的次数,下方是响应函数触发的次数,可以发现在事件大量被触发时(色块密集),响应函数并没有被触发。当事件停止触发一段事件后(三个色块的时间间隔)后,响应函数才会被真正的触发。

如果你想自己试试上面的例子,可以访问这个 codePen

防抖的简单实现

防抖函数其实是一个高阶函数,接收一个函数和防抖事件作为参数,返回值为防抖处理的函数。

的实现其实很简单,我们需要的是是一个定时器,如果在定时器的定时结束前,响应函数被触发的话则重置这个定时器的时间。

 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
function debounce(fn, wait, leading=false) {
  /** @type {number} */
  let timer;
  /** @type {number} */
  let lastCallTime;
  /** @type {boolean} */
  let isInvoked = false;
  return function debounced(...args) {
    const context = this;
    const thisCallTime = Date.now();
    if(leading) {
      if(!isInvoked) {
        fn.apply(context, args);
        isInvoked = true;
      }
      if(thisCallTime - lastCallTime >= wait) {
        fn.apply(context, args);
      }
      lastCallTime = Date.now();
      return;
    }
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(context, args), wait);
  }
}

需要注意的是,上面的函数添加了leading参数。传统的防抖函数响应函数第一次触发也需要等待x ms,使用该参数可让响应函数立即进行触发。

防抖的应用场景

防抖函数的应用场景很多,比如我们需要得到浏览器窗口resize前后窗口的大小。

1
2
3
function onResizeHandler() {
    return  `${window.width()} + ${window.height()}`;
}

当浏览器窗口被拖动的时候,这个响应函数便会触发多次,但是我们实际上想得到的只有resize前和resize后的窗口大小。这时候使用防抖便能很好的解决这个问题。

另一个很经典的使用场景是表单验证,往往表单验证是和input框的onChange绑定在一起的。

1
2
3
function onChange(e) {
    validate(e.target.value);
}

但是如果直接绑定onChange事件,用户的每个字符输入都会触发校验,可能会造成输入卡顿或者给用户抛出错误的校验失败信息,伤害用户体验。这时候如果我们给onChange绑定的是一个经过防抖处理的响应函数,便能很好的避免这种问题了。

什么是节流

节流指的是在x ms内,某一个函数只能被触发一次。

节流的简单实现

节流本质上是通过记录本次调用时间和上次调用时间来实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function throttle(fn, threshold){
 let lastCallTime;
  let isInvoked = false;
  return function throttled(...args) {
    const thisCallTime = Date.now();
    if(!isInvoked) {
      fn.apply(this, args);
      lastCallTime = Date.now();
      isInvoked = true;
    }
    if(thisCallTime - lastCallTime >= threshold) {
      fn.apply(this, args);
      lastCallTime = Date.now();
    }
  }
}

节流的应用场景

节流有一些很经典的应用场景

比如对于一个Button短时间内进行多次点击,可能没有必要触发多次handler,这时候就可以对click的响应函数进行节流处理。

或者一个使用键盘控制的飞机大战的游戏,子弹的射出速度是有限制的,不管你在短时间内触发多少次发射按键,永远只会有一枚子弹被发射。

再或者是在实现无限滚动时,需要去监测内容底部是否已经接近window底部,如果是的话就需要去请求新的内容。关于无限滚动,有一个很棒的 codePen demo。

requestAnimationFrame

在最后要提一下requestAnimationFrame这个浏览器API。这个函数可以理解为 throttle(handler, 16) (16为60fps计算得出) 的浏览器原生实现。当然浏览器不仅做了简单的throttle,还有一些分片和空闲thread监测功能,来保证被这个函数处理的函数能满足每秒60帧的要求。

在某些情况下我们也可以调用这个函数来完成类似throttle的功能。但是需要注意的是

  • 你需要手动触发requestAnimationFrame,但是throttle一旦被设置好后是自动触发的
  • requestAnimationFrame不支持IE9及以下
  • requestAnimationFrame是一个浏览器API,nodejs无法使用。

总结

  • 防抖是指某个函数在空闲x ms后才被调用,如果该段时间内该函数被触发,则重置计时器。常用场景有resize事件或者input框的onChang事件
  • 节流是指某个函数在x ms内只能被触发一次。常用场景有button的click事件或者键盘事件等。
  • requestAnimationFrame是浏览器提供的API,提供了throttle(handler,16)类似的功能但是会有更多浏览器级别的优化来保证每秒16帧的渲染结果。

参考资料

debouncing-throttling-explained-examples

7分钟理解JS的节流、防抖及使用场景

JavaScript 函数节流和函数去抖应用场景辨析