Skip to content

防抖(Debounce)和节流(Throttle)是两种常用于控制频繁事件触发的技术,它们在处理高频率事件(如键盘输入、窗口调整大小、滚动等)时尤为有用。虽然它们有着相似的目标,但在实现原理和适用场景上有所不同。

防抖

防抖(Debounce)是一种控制频繁事件的技术,它确保在一段时间内多次触发同一事件时,只有最后一次事件处理程序被执行。防抖的核心原理是在事件被触发后,设置一个延迟定时器。如果在定时器执行前再次触发事件,则重置定时器,重新计时。这样,只有在事件停止触发一段时间后,处理函数才会被执行。

防抖就好像我们按下一个按钮后启动一个计时器,如果在计时器结束前再次按下按钮,计时器会重新开始计时,只有等到没有再次按下按钮的情况下,计时器结束后才会执行一次操作。

示例场景:

  • 输入搜索框:在用户停止输入一段时间后再发送搜索请求,以避免频繁的请求。
  • 窗口调整大小:在用户停止调整窗口大小一段时间后再执行某些操作。

节流

节流(Throttle)是一种控制频繁事件的技术,它确保在一段时间内只允许某个函数执行一次。节流的核心原理是在第一次触发事件时立即执行处理函数,然后在规定的时间间隔内忽略随后的事件,直到时间间隔结束后再允许执行下一次事件处理

节流就好像我们玩的FPS游戏,像那种单发的枪,不管我们鼠标按得有多快,它规定时间内也只能开一枪。

示例场景:

  • 页面滚动:限制滚动事件的触发频率,每隔100毫秒执行一次事件处理函数,从而减轻浏览器的渲染压力。
  • 按钮点击:防止按钮在短时间内被多次点击,限制按钮点击的频率

再形象点可以如下图表示: 防抖和节流虽然看似相似,但它们的应用场景和实现原理有所不同。接下来,我们将深入探讨这两种技术的具体实现方法、适用场景以及如何在实际开发中灵活运用它们。

代码实现

防抖

javascript
function debounce(func, delay) {
  return function (args) {
    clearTimeout(func.id);
    func.id = setTimeout(() => {
      func(args);
    }, delay);
  };
}
  • 闭包和定时器: debounce 函数返回的是一个闭包,这个闭包保存了 funcdelay 的引用,并且能够访问 func.id,这是一个用于跟踪当前 setTimeout 调用的标识符。每次闭包被调用时,它会首先清除当前的定时器(如果有),然后重新设置一个新的定时器。
  • 清除定时器: clearTimeout(func.id); 这行代码是防抖的关键。每当闭包函数被调用时,它都会取消之前设置的定时器。这意味着只要在 delay 时间内有任何新的事件触发,之前的定时器就会被取消,从而防止了在 delay 时间内多次执行 func
  • 执行时机: 只有在最后一次事件触发后的 delay 时间内没有新的事件发生,func 才会被执行。这是因为每次事件触发都会重置定时器,而 func 只会在定时器最终未被清除的情况下执行。

拿出整个代码来看看效果

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>防抖</title>
  </head>
  <body>
    <div>
      没有防抖的input <input type="text" id="inputA">
    </div>
    <div>
      有防抖的input <input type="text" id="inputB">
    </div>
    <script>
      const inputa = document.getElementById("inputA");
      const inputb = document.getElementById("inputB");
      const ajax = (content) => {
        const currentTime = new Date().toLocaleTimeString();
        console.log(`[${currentTime}] ajax request: ` + content);
      };
      function debounce(func, delay) {
        return function (args) {
          clearTimeout(func.id);
          func.id = setTimeout(() => {
            func(args);
          }, delay);
        };
      }
      inputa.addEventListener('keyup',(e)=>{
        ajax(e.target.value);
      })
      let debounceFunc=debounce(ajax,1000)
      inputb.addEventListener('keyup',(e)=>{
        let value=e.target.value;
        debounceFunc(value)
      })
    </script>
  </body>

</html>

我们模仿一个ajax请求返回时间和内容:

节流

javascript
const throttle=(func,delay)=>{
  // last 记录上一次是啥时候执行的 
  // deferTimer 定时器id
  let last,deferTimer  // 自由变量
  // keyup return func 调用时都能找到闭包中的自由变量
  return (args)=>{
    // 当前时间, 隐式类型转换
    let now=+new Date();
    if(last&&now-last<delay){
      clearTimeout(deferTimer)
      deferTimer=setTimeout(()=>{
        last=now
        func(args)
      },delay)
    }
    else{
      last=now  // 第一次时间
      func(args)  // 先执行一次
    }

  }
}

这段代码实现节流的主要就是:

时间检查:

  • 获取当前时间戳 now
  • 如果自上一次执行以来的时间小于 delay,则不允许立即执行 func
  • 如果允许执行,更新 last 为当前时间戳并执行 func

延迟执行:

  • 如果当前时间与上一次执行时间的差小于 delay,则清除现有的 deferTimer(如果存在),并设置一个新的 setTimeout
  • 新的 setTimeout 将在 delay 毫秒后执行 func 并更新 last

最后的执行: 如果用户停止触发事件(如停止按键),并且最后一个事件触发后的时间超过了 delay,那么在该事件之后不会立即有另一个事件来触发执行。但是,由于我们在延迟执行时设置了 setTimeout,所以即使没有新的触发事件,func 也会在 delay 时间后执行一次,确保了在一定时间内至少执行一次。

我们也给出全部代码来看看实现效果:

html
<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>节流</title>
  </head>

  <body>
    <div class="row">
      <div>
        没有节流的input <input type="text" id="inputa" />
      </div>
      <div>
        节流后的input <input type="text" id="inputc" />
      </div>
    </div>
    <script>
      const inputA = document.getElementById('inputa');
      const inputC = document.getElementById('inputc');


      const ajax = (content) => {
        const currentTime = new Date().toLocaleTimeString();
        console.log(`[${currentTime}] ajax request: ` + content);
      };

      const throttle = (func, delay) => {

        let last, deferTimer

        return (args) => {

          let now = +new Date();
          if (last && now - last < delay) {
            clearTimeout(deferTimer)
            deferTimer = setTimeout(() => {
              last = now
              func(args)
            }, delay)

          }
          else {
            last = now
            func(args)
          }

        }
      }
      inputA.addEventListener('keyup', (e) => {
        ajax(e.target.value);
      })

      let throttledFunc = throttle(ajax, 500)
      inputC.addEventListener('keyup', (e) => {
        let value = e.target.value;

        throttledFunc(value)
      })
    </script>
  </body>

</html>

总结

防抖和节流在控制高频事件方面各有优势:

  • 防抖:适用于那些需要在一段时间内不触发事件后执行操作的场景,例如搜索输入框、窗口调整等。它通过重置定时器,确保事件处理程序只在最后一次触发后的延迟时间内执行。
  • 节流:适用于那些需要控制函数执行频率的场景,例如滚动事件、按钮点击等。它通过设置时间间隔,确保在规定的时间间隔内函数只执行一次。

两者都可以保证最后一次触发的事件可以正常执行;