Skip to content

Instantly share code, notes, and snippets.

@nmsdvid
Created February 4, 2014 16:32
Show Gist options
  • Star 72 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);
@Jungwoo-An
Copy link

this is es6.

function debounceEvent(callback, time) {
  let interval;
  return () => {
    clearTimeout(interval);
    interval = setTimeout(() => {
      interval = null;

      // eslint-disable-next-line
      callback(arguments);
    }, time);
  };
}

@talgoldfus
Copy link

talgoldfus commented Jan 11, 2018

The es6 implementation won't work since the arguments you are sending the callback are the ones called with the anonymous function that is being called by the setTimeout and not the ones you actually want. This should solve this :

function debounceEvent (callback, time) => {
  let interval;
  return (...args) => {
    clearTimeout(interval);
    interval = setTimeout(() => {
      interval = null;
      callback(...args);
    }, time);
  };
};

@akkerman
Copy link

akkerman commented Apr 4, 2018

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?

@hozefaj
Copy link

hozefaj commented Apr 11, 2018

If you want to use arrow syntax, no need to use the function keyowrd

const debounceEvent = (callback, time) => {
  let interval;
  return (...args) => {
    clearTimeout(interval);
    interval = setTimeout(() => {
      interval = null;
      callback(...args);
    }, time);
  };
};

@jimmywarting
Copy link

jimmywarting commented May 21, 2018

@talgoldfus have syntax error (should remove the first =>)

@hozefaj if you do go with arrow function and es6 all in why not make the time have a default value and move the interval variable to the argument so you can get rid of the return keyword and dos minimizing the code even more

const debounceEvent = (callback, time = 250, interval) => 
  (...args) => {
    clearTimeout(interval);
    interval = setTimeout(() => {
      interval = null;
      callback(...args);
    }, time);
  };

And while we are at the verdict on minifying there are some optimizations you can do... you are not using immediate value so we can begin with removing the interval = null thingy since it don't do anything useful

const debounceEvent = (callback, time = 250, interval) => 
  (...args) => {
    clearTimeout(interval);
    interval = setTimeout(() => {
      callback(...args);
    }, time);
  };

now get rid of all unnecessary { }

const debounceEvent = (callback, time = 250, interval) => 
  (...args) => {
    clearTimeout(interval);
    interval = setTimeout(() => callback(...args), time);
  }

we can also safety remove all ; but it's reinserted while minimizing so lets move one line into clearTimeout secound argument do save one byte

const debounceEvent = (callback, time = 250, interval) => 
  (...args) => {
    clearTimeout(interval, interval = setTimeout(() => callback(...args), time));
  }

now it's just 1 line so go ahead and remove the last {}

const debounceEvent = (callback, time = 250, interval) => 
  (...args) =>
    clearTimeout(interval, interval = setTimeout(() => callback(...args), time));

Minified:

const debounceEvent = (a,b=250,c)=>(...d)=>clearTimeout(c,c=setTimeout(()=>a(...d),b))

thanks to @Martin-Pitt

const debounceEvent = (a,b=250,c)=>(...d)=>clearTimeout(c,c=setTimeout(a,b,...d))

and if you also only need to pass one argument to your function (which often is the case of DOM events where only one event argument is passed, then you don't need the spread and it would be fine to just do:

const debounceEvent = (a,b=250,c)=>d=>clearTimeout(c,c=setTimeout(a,b,d))

@PedroUrday
Copy link

If you do not need the "immediate" argument, the function is:

function debounce(callback, time) {
	var timeout;
	return function() {
		var context = this;
		var args = arguments;
		if (timeout) {
			clearTimeout(timeout);
		}
		timeout = setTimeout(function() {
			timeout = null;
			callback.apply(context, args);
		}, time);
	}
}

@Martin-Pitt
Copy link

@jimmywarting's debounce can go a little further because setTimeout has variadic args to pass to the callback, thus skipping an arrow:

const debounceEvent = (callback, time = 250, interval) => 
  (...args) =>
    clearTimeout(interval, interval = setTimeout(callback, time, ...args));

Minified:

const debounceEvent = (a,b=250,c)=>(...d)=>clearTimeout(c,c=setTimeout(a,b,...d))

@netojose
Copy link

netojose commented Dec 9, 2019

Overriding interval parameter causes no-param-reassign eslint error

@jmlivingston
Copy link

Excellent work above! To make it even clearer, I renamed variables related to setTimeout to mimic MDN documentation.

Debounce

const debounce = (callback, delay = 250) => {
  let timeoutId
  return (...args) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      timeoutId = null
      callback(...args)
    }, delay)
  }
}

Test

Calls debounced greet function 10 times, but original fires once as expected.

const greet = () => console.log('Hello World!')
const debouncedGreet = debounce(greet, 3000)

for (let i = 0; i < 10; i++) {
  debouncedGreet()
}

It's also worth checking out lodash's code for debounce and documentation. It's more verbose, but also more optimal as it includes leading and trailing flags like @anurbol's second example above. Just be sure to never import all of lodash due to it's size and import it like this instead: import _debounce from 'lodash/debounce'. I like prefixing with an underscore in case I need to analyze, refactor, or remove any lodash references later. Alternatively, you can just npm install lodash.debounce.

@rowild
Copy link

rowild commented May 30, 2020

Most of these debounce functions are made for a one-time call, right? Whenever it is entered, a clearTimeout is called immediately, which would make it practically unusable with sth like e.g. window.scroll(). For that, the functions using an if(timer) do the desired job.
Or am I mistaken?

@anurbol
Copy link

anurbol commented May 30, 2020

@rowild What do you mean by saying the functions are for one-time call? For one-time call just setTimeout is enough. Or are you talking about something else? You can see the test of my function https://gist.github.com/nmsdvid/8807205#gistcomment-3168449 (at the bottom) - it calls the function 10 times, not 1 time.

@rowild
Copy link

rowild commented May 30, 2020

@anurbol Thanks for your quick feedback!

I have this pen:
https://codepen.io/rowild/pen/JjYgMJX?editors=1010

With a timeout of 1000 I get it to print "hello world" twice. What am I doing wrong?

What confuses me is, that the first thing that happens in the debounce function is to call a clearTimeout. So whenever the function is called, any timeout was was potentially set, is cleared. That's at least, how I understand it – but I am obviously very wrong about that. What I would expect that the clearTimeout should be called in the function, that setTimeout created. As a callback, more or less...

If you could put my head straight, that would be awesome!

@anurbol
Copy link

anurbol commented May 31, 2020

@rowild I am so sorry for the embarassment I caused with my code. Deleted it. It was incorrect and also, I hadn't understand the true concept of debouncing.

@rowild
Copy link

rowild commented May 31, 2020

@anurbol Hey - that wasn't necessary at all!!! It is always good to have examples for discussion and learning processes! And as for myself: my comments are meant as question. Hope that comes through!
However: thank you for responding!

@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