节流/防抖

什么时候需要防抖/节流

在正常开发过程中,特别是在处理用户输入或窗口调整大小等频繁触发的事件时,如果事件的回调函数过于复杂或者是 ajax 请求,在高频调用下难免会出现卡顿。为了限制函数执行频率,一般有两种解决方案:

  1. debounce 防抖
  2. throttle 节流

防抖的原理:在一定时间内函数只调用一次,当防抖函数被触发时,它不会立即执行目标函数,而是设置一个定时器。若在定时器时间到期前再次触发,则取消之前的定时器并重新设置定时器。只有当一段时间内没有新的触发,目标函数才会执行。

从简单的例子入手:

<!DOCTYPE html>
<html>

<head>
    <title>Debounce Example</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
</head>

<body>
    <input id="search" type="text" name="search" placeholder="Search">
    <script src="debounce.js"></script>
</body>

</html>

debounce.js

var inputElement = document.getElementById('search');

function searchValue(ev) {
    // ... search code
    console.log(ev.target.value)
};

inputElement.oninput = searchValue;

在用户停止输入后再进行搜索处理

Version 1

通过防抖的定义简单实现防抖函数:

...

function debounce(func, wait) {
    let timeout;
    return function () {
        clearTimeout(timeout);
        timeout = setTimeout(func, wait);
    };
}

inputElement.oninput = debounce(searchValue, 1000);

Version 2

虽然通过防抖处理使输入停止 1s 后才执行事件,但是执行代码时会发现, event 对象变成 undefined 了。所以使用 apply 方法显式指定 this 的值和参数 debounce v2:

function debounce(func, wait) {
    let timeout;

    return function (...args) {
        clearTimeout(timeout);
        timeout = setTimeout(function () {
            func.apply(this, args)
        }, wait);
    };
}

Version 3

拓展:在之前的版本中都是 在延迟之后执行函数,如果我们希望立即执行函数,并且直到停止触发后,才可以重新执行函数。也就是在延迟之前执行函数。

debounce v3:

function debounce(func, wait, leading) {
    let timeout;
    let result;

    return function (...args) {
        if (timeout) clearTimeout(timeout);
        if (leading) {
            var invoked = !timeout;
            timeout = setTimeout(function () {
                timeout = null;
            }, wait);

            if (invoked) result = func.apply(this, args)
        } else {
            timeout = setTimeout(function () {
                func.apply(this, args)
            }, wait);
        }
        return result;
    };
}

Version 4

拓展:在执行防抖的过程中,有时候我们又会希望在一些特殊情况下可以取消防抖。所以给 debounce 补充 cancel 函数。

debounce v4:

function debounce(func, wait, leading) {
    let timeout;
    let result;

    return function (...args) {

        if (timeout) clearTimeout(timeout);
        if (leading) {
            var invoked = !timeout;
            timeout = setTimeout(function () {
                timeout = null;
            }, wait);
            if (invoked) result = func.apply(this, args)
        } else {
            timeout = setTimeout(function () {
                func.apply(this, args)
            }, wait);
        }
        return result;
    };
    debounce.cancel = function () {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounce;
}
...
const debounceAction = debounce(() => {}, 10000, true);
// 取消防抖
debounceAction.cancel();

Version 5

拓展:在执行防抖的过程中,需要取消防抖并且立即执行。

debounce v5:

function debounce(func, wait, leading) {
    let timeout;
    let result;

    return function (...args) {

        if (timeout) clearTimeout(timeout);
        if (leading) {
            var invoked = !timeout;
            timeout = setTimeout(function () {
                timeout = null;
            }, wait);

            if (invoked) result = func.apply(this, args)
        } else {
            timeout = setTimeout(function () {
                func.apply(this, args)
            }, wait);
        }
        return result;
    };
    debounce.cancel = function () {
        clearTimeout(timeout);
        timeout = null;
    };

    debounced.flush = function() {
    if (timeout) {
      clearTimeout(timeout);
      func.apply(this, arguments);
      timeout = null;
    }
  };

    return debounce;
}
...
const debounceAction = debounce(() => {}, 10000, true);
debounceAction.flush();

节流

与防抖(debounce)不同,防抖是在最后一次调用结束后的延迟时间后才执行,而节流是确保函数在规定的时间间隔内至多执行一次。

节流的实现通常有两种方式:使用时间戳和使用定时器。

使用时间戳实现节流

使用时间戳的节流函数会在每次事件触发时,检查当前时间与上次执行时间的间隔,如果超过了设定的时间间隔,就执行函数。

function throttle(func, wait) {
  let previous = 0;
  const context = this;
  const args = arguments;

  return function (...args) {
    const now = Date.now();
    if (now - previous > wait) {
      previous = now;
      func.apply(context, args);
    }
  };
}

使用定时器实现节流

在第一次事件触发时执行函数,并设置一个定时器,在设定的时间间隔之后允许下一次执行。

function throttle(func, wait) {
  let timeout = null;

  return function(...args) {
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(this, args);
      }, wait);
    }
  };
}

version 2

对于两种方法实现节流都有一些缺点,使用时间戳在停止触发之后没办法再次执行函数,而使用定时器没办法再一开始就执行函数。综合两种方法的特点,可以写出 throttle v2:

function throttle(func, wait) {
    let timeout,
    previous = 0;

    const throttled = function (...args) {
    const now = Date.now();
    const remaining = wait - (now - previous);

    if (remaining <= 0 || remaining > wait) {
        if (timeout) {
            clearTimeout(timeout);
            timeout = null;
        }
        previous = now;
        func.apply(this, args);
    } else if (!timeout) {
        timeout = setTimeout(() => {
            previous = Date.now();
            timeout = null;
            func.apply(this, args);
        }, remaining);
    }
    };
    // 添加 `cancel` 方法,用于取消节流操作
    throttled.cancel = function () {
        clearTimeout(timeout);
        timeout = null;
        previous = 0;
    };

    return throttled;
}

version 3

拓展:添加 Boolean 控制是否需要首次或者是否需要停止触发后再次执行函数。
throttle v3:

function throttle(func, wait, options = {}) {
  let timeout, previous = 0;
  const { leading = true, trailing = true } = options;

  const throttled = function(...args) {
    const now = Date.now();

    if (!previous && !leading) previous = now;

    const remaining = wait - (now - previous);

    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(this, args);
    } else if (!timeout && trailing) {
      timeout = setTimeout(() => {
        previous = leading ? Date.now() : 0;
        timeout = null;
        if (trailing) func.apply(this, args);
      }, remaining);
    }
  };

  throttled.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
    previous = 0;
  };

  return throttled;
}

如果传入 {leading: false, trailing: false} 节流函数不会执行任何操作。因为不会再第一次事件触发时执行 且 不会再最后一次事件触发后等待间隔再执行,这意味着没有条件可以触发他。