← Download MyJS

MyJS block

debounce 1.0.0

Makes a debounced function wrapper for the given afterWait function.

Download with this
Exports
debounce
Works in
Browser and Node.js
The code of version 1.0.0
/**
 * Called by a debounced function: `immediate` on every call, and `afterWait`
 * once the calls stop (or `maxWaitMS` passes). It gets the arguments of the
 * call (for `afterWait`, the latest call), and that call's `this`.
 *
 * @callback DebounceCallback
 * @param {...*} args The arguments the debounced function was called with.
 * @returns {*} Ignored.
 */

/**
 * A function made by {@link debounce}. Calling it calls `immediate` right away
 * and (re)starts the wait for `afterWait`.
 *
 * @typedef {Function} DebouncedFunction
 * @property {function(): boolean} cancel Drops the pending call to
 *   `afterWait`, if there is one (e.g. when what it would update is gone).
 *   Returns `true` if there was one to drop.
 * @property {function(): boolean} flush Calls `afterWait` right away with the
 *   latest arguments, if a call is pending, instead of waiting. Returns `true`
 *   if it called it.
 */

/**
 * Makes a debounced function: however often it's called, `afterWait` is
 * only called once the calls have stopped for `waitMS` milliseconds, with
 * the arguments of the latest call. With `maxWaitMS`, it's also called at
 * least every `maxWaitMS` milliseconds while the calls keep coming, so a
 * steady stream of calls can't put it off forever.
 *
 * `immediate`, if given, is called on every call, right away. If it throws,
 * the debounce carries on and the error is rethrown separately (so it shows
 * in the console without stopping anything).
 *
 * @param {Object} options
 * @param {number} options.waitMS How long the calls have to stop, in
 *   milliseconds, before `afterWait` is called.
 * @param {DebounceCallback} options.afterWait Called once the calls stop
 *   (or `maxWaitMS` passes), with the latest call's arguments and `this`.
 * @param {DebounceCallback} [options.immediate] Called on every call, right
 *   away, with that call's arguments and `this`.
 * @param {number} [options.maxWaitMS] The longest `afterWait` can be put off
 *   while calls keep coming, in milliseconds. Leave it out (or use 0) for no
 *   limit.
 * @returns {DebouncedFunction} The debounced function, with `cancel()` and
 *   `flush()`.
 * @throws {TypeError} If `afterWait` isn't a function, `waitMS` isn't a
 *   number of milliseconds, or `immediate` or `maxWaitMS` is given but isn't one.
 *
 * @example
 * // Search once typing pauses for 300ms, but at least every second
 * const search = debounce({
 *   waitMS: 300,
 *   maxWaitMS: 1000,
 *   immediate: () => spinner.show(),
 *   afterWait: (text) => runSearch(text),
 * });
 * input.addEventListener('input', () => search(input.value));
 *
 * @example
 * // Save right away when the page is being left, and stop when it's gone
 * window.addEventListener('pagehide', () => save.flush());
 * component.onDestroy(() => save.cancel());
 */
function debounce({ waitMS, afterWait, immediate, maxWaitMS }) {
  if (typeof afterWait !== 'function') {
    throw new TypeError('"afterWait" must be a function.');
  }
  if (typeof waitMS !== 'number' || !(waitMS >= 0)) {
    throw new TypeError('"waitMS" must be a number of milliseconds.');
  }
  if (immediate != null && typeof immediate !== 'function') {
    throw new TypeError('"immediate" must be a function if it\'s given.');
  }
  if (maxWaitMS != null && (typeof maxWaitMS !== 'number' || !(maxWaitMS >= 0))) {
    throw new TypeError('"maxWaitMS" must be a number of milliseconds if it\'s given.');
  }

  // The pending call (if any): its timers, and the latest call's arguments and this
  let timeoutID, maxTimeoutID, lastArgs, lastThis;
  let isPending = false;

  /**
   * Forgets the pending call and stops its timers.
   *
   * @private
   * @returns {{args: Array, thisArg: *}} The latest call's arguments and this.
   */
  function clear() {
    clearTimeout(timeoutID);
    clearTimeout(maxTimeoutID);
    const pending = { args: lastArgs, thisArg: lastThis };
    timeoutID = maxTimeoutID = lastArgs = lastThis = undefined;
    isPending = false;
    return pending;
  }

  /**
   * Calls afterWait for the pending call. The state is cleared first, so
   * afterWait can call the debounced function again (starting a new wait).
   *
   * @private
   */
  function run() {
    const { args, thisArg } = clear();
    afterWait.apply(thisArg, args);
  }

  /**
   * @private
   * @type {DebouncedFunction}
   */
  const debounced = function (...args) {
    if (immediate) {
      try {
        immediate.apply(this, args);
      } catch (error) {
        // Don't let it stop the debounce, but don't hide it either
        setTimeout(() => { throw error; });
      }
    }

    lastArgs = args;
    lastThis = this;
    clearTimeout(timeoutID);
    timeoutID = setTimeout(run, waitMS);
    if (maxWaitMS && !isPending) {
      maxTimeoutID = setTimeout(run, maxWaitMS);
    }
    isPending = true;
  };

  debounced.cancel = function () {
    const wasPending = isPending;
    clear();
    return wasPending;
  };

  debounced.flush = function () {
    if (!isPending) {
      return false;
    }
    run();
    return true;
  };

  return debounced;
}

Versions

  1. 1.0.0 Oct 16, 2026

    Initial

JSDoc