Skip to content

Instantly share code, notes, and snippets.

@nmsdvid
Created February 4, 2014 16:32
Show Gist options
  • Star 71 You must be signed in to star a gist
  • Fork 17 You must be signed in to fork a gist
  • Save nmsdvid/8807205 to your computer and use it in GitHub Desktop.
Save nmsdvid/8807205 to your computer and use it in GitHub Desktop.
Simple JavaScript Debounce Function
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (immediate && !timeout) func.apply(context, args);
};
}
/* minified */
function debounce(a,b,c){var d;return function(){var e=this,f=arguments;clearTimeout(d),d=setTimeout(function(){d=null,c||a.apply(e,f)},b),c&&!d&&a.apply(e,f)}}
/* usage */
var myEfficientFn = debounce(function() {
// All the taxing stuff you do
}, 250);
window.addEventListener('resize', myEfficientFn);
@anurbol
Copy link

anurbol commented Jun 1, 2020

@rowild

Well my deleted functions actually did throttling, not debouncing, and they also had a bug.

This is a simple debounce function I came with:

var debounce = (callback, wait = 250) => {
  let timer;
  return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => callback(...args), wait);
  };
};

// Testing it:

var d = debounce(() => console.log("CALLED"), 1000);

for (let i = 0; i < 10; i++) {
    // call every 100 ms (change to 1500 to see that debouncing will not happen)
    let timeOut = 100 * i;

    const cb = () => {
        const date = new Date();
        console.log(`calling at ${date.getSeconds()}.${date.getMilliseconds()}`);
        d()
    }
    
    setTimeout(cb, timeOut)
}

One of the common usages is implementing an auto-completion for an input that will not overwhelm the server with requests.

Aaand yes let's discuss!

Regarding the position of clearTimeout: it's dead simple - we cancel the previous timer i.e. previously scheduled function (if any - otherwise it's a no-op) just before we create the new one. We do it to prevent the previously scheduled function from being executed, because we don't want that, we only want the function/callback to be executed once (e.g. after user ends typing into the input), if the calls are happening frequently enough. Placing clearTimeout inside the scheduled function doesn't make sense, because this will lead to the situation when we clear the timeout too late, the function would have already been executed.

Hope that makes sense! If no, please ask, or maybe correct me.

@casulit
Copy link

casulit commented Jul 6, 2020

If we write this in TS? what would be the type of callback and ...args

@eduardomoroni
Copy link

interface DebouncedFunction {
  (): any;
  cancel: () => void;
}

export const debounce = <F extends (...args: any[]) => ReturnType<F>>(
  func: F,
  wait: number,
  immediate?: boolean
) => {
  let timeout: number = 0;

  const debounced: DebouncedFunction = function(this: void) {
    const context: any = this;
    const args = arguments;

    const later = function() {
      timeout = 0;
      if (!immediate) func.call(context, ...args);
    };
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = window.setTimeout(later, wait);
    if (callNow) func.call(context, ...args);
  };

  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = 0;
  };

  return debounced as (...args: Parameters<F>) => ReturnType<F>;
};

@oznekenzo
Copy link

If found that, when setting immediate to true the debounced function was not invoked at all.
This is because as soon as you get at if (immediate && !timeout) the timeout variable was just set.
Maybe move that if statement to before setting timeout?

This 👆

@nicograef
Copy link

nicograef commented Oct 26, 2021

My TypeScript solution:

function debounce(func: (...args: unknown[]) => unknown, delay = 200) {
  let timeout: number;

  return function (...args: unknown[]) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), delay);
  };
}

or maybe (also when running in node or web)

function debounce<T>(func: (...args: T[]) => unknown, delay = 200) {
  let timeout: number | NodeJS.Timeout;

  return function (...args: T[]) {
    clearTimeout(timeout as number);
    timeout = setTimeout(() => func(...args), delay);
  };
}

@geoffreyhale
Copy link

geoffreyhale commented Jan 9, 2022

I suggest adding to our debounce function return type unknown or I prefer typeof func as shown on line 4 below:

function debounce<T>(
  func: (...args: T[]) => unknown,
  delay = 200
): typeof func {
  let timeout: number | NodeJS.Timeout;
  return function (...args: T[]) {
    clearTimeout(timeout as number);
    timeout = setTimeout(() => func(...args), delay);
  };
}

Without it results may be faced with type conversion issues a la ts(2352).

@MominBinShahid
Copy link

MominBinShahid commented Feb 13, 2022

As indicated above, in the main gist function, using immediate will break the implementation
So, for such needs where we wanted to call the function immediately (leading edge instead of trailing)
Try this implementation https://youmightnotneed.com/lodash#function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment