← Download MyJS
MyJS block
debounce 1.0.0
Makes a debounced function wrapper for the given afterWait function.
- 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.0.0 Oct 16, 2026
Initial