skip to content
OnionTalk

Js中的防抖与节流

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

为什么需要防抖和节流?

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

function onResizeHandler() {
  // do something on resize
}

window.addEventListener('resize', onResizeHandler);

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

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

什么是防抖

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

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

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

防抖的简单实现

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

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

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 前后窗口的大小。

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

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

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

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

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

什么是节流

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

节流的简单实现

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

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 函数节流和函数去抖应用场景辨析