基础学习: debounce

防抖函数学习,从背景,源码到实现
更新于: 2023-09-05 09:20:25

简介

Debounce是一种通过延迟函数执行来避免重复执行,从而提高代码性能和响应性的技术。

简单点:高频率触发,但只执行最后一次,前端场景: input → autocomplete

在 JavaScript 中,debounce(防抖)是一种常用的技术,可以延迟函数的执行时间,直到某个特定事件停止触发。当需要监听某个事件的时候,比如用户输入或者滚动页面,如果每次事件都立即执行相应的函数,可能会导致性能问题或者意外的行为。通过使用debounce,可以限制事件的触发次数,并确保相应的函数只在事件停止触发一段时间后才会被执行,从而减少不必要的执行。

实现debounce最常用的方式是通过设置一个定时器来实现。当事件被触发时,如果定时器已经存在,则清除该定时器,然后重新设置一个新的定时器,使得函数的执行被推迟到一定的时间之后。如果在这段时间内,事件再次触发,就会清除之前的定时器,并重新设置一个新的定时器。只有在事件停止触发一段时间后,定时器才会触发相应的函数。

来源

"Debounce"这个词源于电子学的术语。在电路设计中,防抖电路用于消除电子元件接触时产生的抖动(bounce)信号。当开关或按钮被按下时,由于机械元件的反弹和震动,可能会导致电路在短时间内多次开闭,产生抖动信号,这会对电路的正常工作造成干扰。因此,防抖电路被用来延迟电路响应,直到稳定信号出现。

类比电子学中的防抖电路,JavaScript 中的 debounce 可以理解为对于用户输入或其他事件的抖动,通过延迟函数执行来确保只执行一次,从而减少不必要的计算和优化代码性能。

至于为什么JavaScript中采用了这个术语,可能是因为许多前端开发者也是电子工程师,因此借鉴了电子学中的防抖术语来描述 JavaScript 中的这种技术。

基本实现

  • 实际上这里的 context = this 是不需要的,因为后面用的是 arrow function
  • 另外: args 也可以用 …args 直接取到
function debounce(func, delay) {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

存在的问题(改进版本)

  • 每次会返回一个闭包函数,当数量较多的情况下,会出现内存泄漏问题
  • 每次会创建一个新的函数,但有 cancel 方法,用户可以在不需要的时候手动清除

这种实现方式会每次返回一个新的闭包函数,并且每个闭包函数都会持有一个对 timerId 的引用。这样会导致内存使用量增加,如果过多地创建这种闭包函数,可能会导致内存泄漏或性能问题。

为了避免这种情况,我们可以将 timerId 存储在函数的闭包中,这样就不必每次都创建一个新的闭包函数。修改后的代码如下:

function debounce2(func, delay) {
  let timer;

  function debounced(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  }

  debounced.cancel = function () {
    clearTimeout(timer);
  };

  return debounced;
}

timerId=null 解释

timerId = null; 这一行代码和 clearTimeout(timerId); 的区别在哪里?

根据测试,timer是分配一个随机数字id,clearTimeout后,timer的变量指向数字id还在, 只是定时器停止了。
aric: 把timer赋值为null,是为了释放内存

timerId = null;clearTimeout(timerId); 都是用来清除定时器的,但是它们的作用不完全相同。

clearTimeout(timerId); 是用来取消还未执行的定时器,即在计时器到达指定时间之前,调用 clearTimeout 函数可以取消定时器的执行。这个函数接收一个参数 timerId,这个参数是通过调用 setTimeout 函数返回的定时器 ID。

timerId = null; 是将变量 timerId 的值设置为 null。在 debounced 函数中,当定时器的回调函数被执行完毕后,我们将 timerId 设置为 null 的作用是,使得下一次调用 debounced 函数时可以知道上一次的执行已经结束,可以重新设置定时器。也就是说,timerId = null; 并不是取消定时器,而是重置定时器的状态。

综上所述,clearTimeout(timerId);timerId = null; 的作用是不同的,前者用来取消定时器的执行,后者用来重置定时器的状态。在实现防抖功能时,我们需要用到这两个操作。在 debounced 函数中,我们先调用 clearTimeout 取消上一次的定时器,然后重新设置定时器并在回调函数执行完毕后将 timerId 设置为 null,这样就可以实现防抖的效果。

关于 timerID

在浏览器中,定时器标识符是一个数字。在其他环境中,可能是其他的东西。例如 Node.js 返回的是一个定时器对象,这个对象包含一系列方法,这些方法没有统一的规范定义,所以这没什么问题。

nodejs 环境的 timerID
浏览器端的 timerID

最小延时4ms

 最小延时, DOM_MIN_TIMEOUT_VALUE, 是 4ms (但在 Firefox 中通常是是存储在 dom.min_timeout_value 这个变量中), DOM_CLAMP_TIMEOUT_NESTING_LEVEL 的第 5 层。

最大延时值

包括 IE、Chrome、Safari、Firefox 在内的浏览器其内部以 32 位带符号整数存储延时。这就会导致如果一个延时 (delay) 大于 2147483647 毫秒 (大约 24.8 天) 时就会溢出,导致定时器将会被立即执行。

0延时定时器

因为用到了 postMessage,所以,web 环境上是可以使用的,npm 上已经有别人发布好的版本了。

https://www.npmjs.com/package/zero-timeout

(function () {
  var timeouts = [];
  var messageName = "zero-timeout-message";

  // Like setTimeout, but only takes a function argument.  There's
  // no time argument (always zero) and no arguments (you have to
  // use a closure).
  function setZeroTimeout(fn) {
    timeouts.push(fn);
    window.postMessage(messageName, "*");
  }

  function handleMessage(event) {
    if (event.source == window && event.data == messageName) {
      event.stopPropagation();
      if (timeouts.length > 0) {
        var fn = timeouts.shift();
        fn();
      }
    }
  }

  window.addEventListener("message", handleMessage, true);

  // Add the one thing we want added to the window object.
  window.setZeroTimeout = setZeroTimeout;
})();

重新实现 debounce

本次话题来源: 渡一课程

  • 频繁调用某个函数
  • 造成效率问题
  • 需要的结果以最后一次调用为准

手写

  • 当浏览器 resize 的时候,执行 layout() 函数
// 原来的情况
function layout() {
  console.log("layout");
}

window.onresize = function () {
  layout();
};

// -------- 我是分隔线,下面是优化后的版本(原汁原味的debounce实现) ---------
var timerId;

window.onresize = function () {
  clearTimeout(timerId);
  timerId = setTimeout(()=>{
    layout();
  }, 500);
};

// --- 抽象 debounce 函数
// this: 指返回的函数的this
// arguemnts: 指传入参数
function debounce(fn, wait) {
  var timerId;
  return function () {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn.apply(this, arguments);
    }, wait);
  };
}

完整demo

比较简单的测试一些case.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Demo for debounce</title>
  </head>
  <body>
    <script>
      function layout() {
        console.log("layout");
      }

      // -------- 我是分隔线,下面是优化后的版本(原汁原味的debounce实现) ---------
      var d_layout = debounce(layout, 300);

      window.onresize = d_layout;

      // --- debounce
      function debounce(fn, wait) {
        var timerId;
        return function () {
          clearTimeout(timerId);
          timerId = setTimeout(() => {
            fn.apply(this, arguments);
          }, wait);
        };
      }
    </script>
  </body>
</html>

参考