// 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); |
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);
};
};
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
?
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);
};
};
@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))
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);
}
}
@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))
Overriding interval
parameter causes no-param-reassign eslint error
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.
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?
@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.
@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!
@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.
@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!
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.
If we write this in TS? what would be the type of callback and ...args
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>;
};
If found that, when setting
immediate
totrue
the debounced function was not invoked at all.
This is because as soon as you get atif (immediate && !timeout)
thetimeout
variable was just set.
Maybe move that if statement to before settingtimeout
?
This 👆
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);
};
}
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).
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
this is es6.