When writing a debounce, what is the point of clearTimeout? - javascript

debounceFunction() {
let timeout = setTimeout(() => {
doSomething();
flag = true;
clearTimeout(timeout);
}, 250);
}
I wrote a debounce function that looks like the above, I called this function for several times when an event is triggered. My question is, does the clearTimeout at the end of the setTimeout makes any sense?
What would be the optimal way to do it?
Thanks in advance :)

Does the clearTimeout at the end of the setTimeout make any sense?
No, there's no point to call clearTimeout from within the setTimeout callback - it's too late there already. You cannot clear anything, the timeout already occurred.
clearTimeout is used when you want to prevent the callback from getting called before that would happen.
What would be the optimal way to do it?
Just omit it.
If you are asking about the optimal way to write debounce, see Can someone explain the "debounce" function in Javascript.

My question is, does the clearTimeout at the end of the setTimeout
makes any sense?
No, clearTimeout is used to clear the previous created timeout, so clearTimeout should be done before setTimeout in order to cancel out previous invocation of setTimeout.
What would be the optimal way to do it?
Have a look David Walsh's post and SO question
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};

clearTimeout() prevents the execution of the function that has been set with setTimeout();
In debouncing, a user is made to perform limited actions in a time interval.
So we set that action inside a function within setTimeout and whenever the user tries to perform the same action within the given interval, we call clearTimeout to prevent user from doing it.
So clearTimeout should be called before SetTimeout.

Related

Javascript event execution deferred in a queue

I'm trying to explain my problem to know the better way to solve it. I've searching a bit, but I don't know how to search exactly:
I have an HTML page with three areas: Panel A, Grid B and Grid C.
On grid C, I can do an action on a row (only clicking it) that updates some counters on panel A and Grid B, but they're calculated on database totals.
When I do the row action I update the row immediately and trigger an event listened by Panel A and Grid B which sends both requests against the server to update it's counters.
Every row update is a bit heavy and if the user clicks various rows fast, the javascript execution is locked flooding the server with updates of Panel A and Grid B which could be deferred to execute only one time if on 1 or 2 seconds the event is not triggered.
I would solve the problem on the listenTo callback because it could be another panel that the event action must be performed "immediately".
I imagine something like this (only refresh after 2 seconds of no event listened), but I think that there must be a better way:
var eventTimeout = {}; // one for listener
element.bind('eventName' function() {
if (eventTimeout['eventName']) {
clearTimeout(eventTimeout['eventName']); // I understand that if the timeout has been exhausted no error is thrown
}
eventTimeout['eventName'] =
setTimeout(function() {
eventTimeout['eventName'] = null;
doAction();
}, 2000);
});
I'll go away with that implementation (I haven't tested yet), when I have more time, I'll put it on a JSFiddle to help to understand.
You are on the right track with your code but you may want to use something like lodash-throttle function decorators rather than reinventing the wheel here IMO.
lodash Throttle
Creates a throttled function that only invokes func at most once per every wait milliseconds. The throttled function comes with a cancel method to cancel delayed invocations. Provide an options object to indicate that func should be invoked on the leading and/or trailing edge of the wait timeout. Subsequent calls to the throttled function return the result of the last func call.
examples from their own site:
// avoid excessively updating the position while scrolling
jQuery(window).on('scroll', _.throttle(updatePosition, 100));
// invoke `renewToken` when the click event is fired, but not more than once every 5 minutes
jQuery('.interactive').on('click', _.throttle(renewToken, 300000, {
'trailing': false
}));
// cancel a trailing throttled call
jQuery(window).on('popstate', throttled.cancel);
Using the previous #bhantol very valuable response, and some other stackoverflow responses (https://stackoverflow.com/a/43638411/803195) I've published a sample code that simulates the behavior I actually want.
Perhaps it was not well defined on initial question, but I need actually use debounce and it must be dynamic, depending on some variables (a checkbox on the following sample) it must be "delayed" or "immediate":
https://codepen.io/mtomas/pen/xYOvBv
var debounced = _.debounce(function() {
display_info($right_panel);
}, 400);
$("#triggerEvent").click(function() {
if (!$("#chk-immediate").is(":checked")) {
debounced();
} else {
display_info($right_panel, true);
}
});
The sample is based on a original sample published on that (interesting) article:
https://css-tricks.com/debouncing-throttling-explained-examples/
-- UPDATE --
Using debounce of lodash implies me to import full lodash (72Kb minimized), so I've implemented a "lite" own debounce using this reference:
https://davidwalsh.name/function-debounce
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
I've updated my codepen test too.

Timeout JavaScript function after being clicked

I have replaced the iframes on my website with AJAX. It's a lot better now and a lot faster. People can click the refresh button to refresh the dynamic areas.
I am using this function for that:
function djrefresh() {
$('#dj_status').load('inc/dj_status_frame.php');
$('#djbanner').load('inc/djbanner.php');
$('#djknopjes').load('inc/dj_knopjes_frame.php');
$('#djzegt').load('inc/dj_zegt_frame.php');
$('#djfooter').load('inc/footer_frame.php');
$('#berichtenbalkframe').load('inc/berichtenbalk_frame.php');
}
Works perfectly fine, but my site needs to load a lot of stuff all at once. I want the user to be able to click it once and get a timeout for 30 seconds.
... or if you have a better idea please tell me. I don't want the user to DDOS my website with my own scripts. Thanks in advance.
Changing your sites arcitcture is probably the best option, but without more information it's difficult to give any recommendations. Anyhow, to limit calls to djrefresh you can use a debounce function. UnderscoreJS includes the function or you can write one yourself.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
This is taken from https://davidwalsh.name/javascript-debounce-function (I've used it quite a bit personally).
This assumes the "refresh button" is a button the page, not the browser refresh.
Edit: If you do have a refresh button on your site, it would be simpler to just disable it for 30 seconds after it has been clicked.
Just create a count of the calls, use a callback on the calls, if the load has finished on all of them then allow the function to continue.
var djrefresh;
//close values to reduce variable name collision
(function(){
var locked = false;
var callcount = 0;
djrefresh = function() {
if( locked ) return;
locked = true;
$('#dj_status').load('inc/dj_status_frame.php',unlock);
$('#djbanner').load('inc/djbanner.php',unlock);
$('#djknopjes').load('inc/dj_knopjes_frame.php',unlock);
$('#djzegt').load('inc/dj_zegt_frame.php',unlock);
$('#djfooter').load('inc/footer_frame.php',unlock);
$('#berichtenbalkframe').load('inc/berichtenbalk_frame.php',unlock);
}
function unlock(){
if( ++callcount == 6 ) locked = false;
}
})()

Is it a good idea to use requestAnimationFrame within a debounce function?

This is a check on my understanding of requestAnimationFrame. I have a need for a debounce function, as I'm doing some DOM interaction every time the window is resized and I don't want to overload the browser. A typical debounce function will only call the passed function once per interval; the interval is usually the second argument. I'm assuming that for a lot of UI work, the optimum interval is the shortest amount of time that doesn't overload the browser. It seems to me that that's exactly what requestAnimationFrame would do:
var debounce = function (func, execAsap) {
var timeout;
return function debounced () {
var obj = this, args = arguments;
function delayed () {
if (!execAsap)
func.apply(obj, args);
timeout = null;
};
if (timeout)
cancelAnimationFrame(timeout);
else if (execAsap)
func.apply(obj, args);
timeout = requestAnimationFrame(delayed);
};
}
The above code is a direct rip-off from the above debounce link, but with requestAnimationFrame used instead of setTimeout. In my understanding, this will queue up the passed-in function as soon as possible, but any calls coming in faster than the browser can handle will get dropped. This should produce the smoothest possible interaction. Am I on the right track? Or am I misunderstanding requestAnimationFrame?
(Of course this only works on modern browsers, but there are easy polyfills for requestAnimationFrame that just fall back to setTimeout.)
This will work.
It has a caveat that may or may not be important to you:
If the page is not currently visible, animations on that page can be throttled heavily so that they do not update often and thus consume little CPU power.
So if you for some reason care about this for the function you are debouncing, you are better off using setTimeout(fn, 0)
Otherwise if you are using this for animations, this is the intended usage of requestAnimationFrame

Run a function in timeout looping

I have the following code:
// After 8 seconds change the slide...
var timeout = setTimeout("changeSlide()", 8000);
$('div.slideshow').mouseover(function() {
// If the user hovers the slideshow then reset the setTimeout
clearTimeout(timeout);
});
$('div.slideshow').mouseleave(function() {
clearTimeout(timeout);
var timeout = setTimeout("changeSlide()", 8000);
});
What I want to happen is make the function changeSlide run EVERY 8 seconds in a loop unless someone hovers the slideshow div. When they remove the cursor then do the timeout again!
However the loop only happens once and the hover doesn't stop the timeout or start it again :/
EDIT:
This loops great but the hover on and off causes the function to run multiple times:
// After 8 seconds change the slide...
var timeout = setInterval(changeSlide, 2000);
$('div.slide').mouseover(function() {
// If the user hovers the slideshow then reset the setTimeout
clearInterval(timeout);
});
$('div.slide').mouseleave(function() {
clearInterval(timeout);
var timeout = setInterval(changeSlide, 2000);
});
You have a couple issues here. First off, when you set a timeout, you need to store the return of that function call into a variable if you potentially want to stop it.
var slide_timer = setTimeout(changeSlide, 8000);
Second, when you call clearTimeout (rather than clearInterval), you need to pass it an argument. What argument? That variable you stored when you called setTimeout
clearTimeout(slide_timer);
Third, when you use setTimeout, it only fires once. setInterval will continue to fire, then you'd use clearInterval to stop it.
There is an issue in timing with using intervals rather than timeouts. The browser treats them subtly differently, and it may be important to your code to know the difference and use the proper method. If you use intervals, since they only fire once, you'll have to re-establish the timeout every time it fires.
var slide_timer = setTimeout(function () {
changeSlide();
var slide_timer = setTimeout(changeSlide, 8000);
}, 8000);
OR
var slide_timer = setTimeout(changeSlide, 8000);
...
function changeSlide() {
... your code ...
var slide_timer = setTimeout(changeSlide, 8000);
}
(I prefer the former method)
And lastly, whether you use timeouts or intervals, don't pass a string to setTimeout, pass a function reference. See the sample code above, or like this:
var slide_timer = setTimeout("changeSlide()", 8000); // <--- DON'T
var slide_timer = setTimeout(changeSlide, 8000); // <--- DO
var slide_timer = setTimeout(function () { // <--- DO
changeSlide() ;
// other script
}, 8000);
Putting it all together:
// After 8 seconds change the slide...
var slide_timer = setTimeout(changeSlide, 8000);
$('div.slideshow').hover(function() {
// If the user hovers the slideshow then reset the setTimeout
clearTimeout(slide_timer);
}, function() {
slide_timer = setInterval(changeSlide, 8000);
});
Documentation
clearTimeout - https://developer.mozilla.org/en/DOM/window.clearTimeout
setTimeout - https://developer.mozilla.org/en/DOM/window.setTimeout
clearInterval - https://developer.mozilla.org/en/DOM/window.clearInterval
setInterval - https://developer.mozilla.org/en/DOM/window.setInterval
SO answer discussing the subtle difference between intervals and timeouts - https://stackoverflow.com/a/7900293/610573
When you specify setTimeout (or setInterval), it returns a value that is then used for clearTimeout and clearInterval. Correct usage is as follows:
var timeout = setTimeout(changeSlide, 8000);
clearTimeout(timeout);
Also note I am using clearTimeout, not clearInterval.
You'll also notice that I did not put quotes around 'changeSlide', and that I dropped the parens. When passing a string to setTimeout, eval() is used. eval() is, in general, recommended to be avoided. So, instead, we pass it the direct reference to the function (without quotes). We do not use parens, because that would actually call changeSlide() right away, instead of deferring execution to setTimeout (and would pass, as an argument to setTimeout, the result of changeSlide())
EDIT: To get it to run continously, you have to call setTimeout again after each changeSlide call. setTimeout runs once. As an alternative, you can use setInterval, which automatically repeats. The one caveat to setInterval is that if the interval is too short and the callback it calls takes a long time to run, you can end up with a bunch of intervals queued up to execute one after another, without delay. An 8 second interval would likely not face this problem.
EDIT 2:
var timeout;
var changeSlide = function(){
// do something to change slides
clearTimeout(timeout);
timeout = setTimeout(changeSlide, 8000);
}
// queue up the first changeSlide, all others happen inside changeSlide
timeout = setTimeout(changeSlide, 8000);
$('div.slideshow').mouseleave(function() {
clearTimeout(timeout);
var timeout = setTimeout(changeSlide, 8000);
});
I think you need setInterval and not setTimeout, since:
setTimeout(expression, timeout); runs the code/function once after the timeout
setInterval(expression, timeout); runs the code/function in intervals, with the length of the timeout between them
In the end this worked the best (but I won't accept this as my answer as it was others who helped me get to this point)
// After 8 seconds change the slide...
var slide_timer = setInterval(changeSlide, 2000);
$('div.slideshow').hover(function() {
// If the user hovers the slideshow then reset the setTimeout
clearInterval(slide_timer);
}, function() {
slide_timer = setInterval(changeSlide, 2000);
});
Your issue may be the mouseover event. The mouseover event bubbles, so if you have a nested HTML structure, then the event may be called more than once. If you are using jQuery, you should use the mousenter event, which will only get called once for the element.
On a different note, instead of using setInterval use a setTimeout pattern. Something like this:
//Immediately Invoked Function Expression that gets called immediately
(function() {
//Make the initial call to your setTimeout function
repeat();
//Function will constantly be called
function repeat() {
setTimeout(function() {
//(Put your conditional logic here)
console.log('test');
repeat();
}, 2000);
}
})();

Prevent and queue action (but only once globally) if previously called within X seconds

I always run into this problem and seem to implement a nasty looking solution.
It seems like a common design pattern to fire an action immediately, but not let that action queue up if clicked rapidly / delay firing if previously called within a timeframe. In my real world example, I have an AJAX call being made, so if I don't prevent repetitive actions the browser queues requests.
How would you implement this differently? What other options are there?
function myFunction() {
console.log("fired");
}
var timeout = null;
$("#foo").click(function() {
// if not previously clicked within 1 second, fire immediately
if (!timeout) {
myFunction();
timeout = setTimeout(function() {
timeout = null;
}, 1000);
} else {
// clicked again within 1s
clearTimeout(timeout); // clear it - we can't have multiple timeouts
timeout = setTimeout(function() {
myFunction();
timeout = null;
}, 1000);
};
});
With your current code, if you repeatedly click "#foo" at an interval slightly less than one second, say every 800ms, on first click it will fire the function immediately (obviously), but then it will fire the function exactly once more one second after the last click. That is, if you click ten times at 800ms intervals the function will fire once immediately and a second time approximately 8 seconds (800ms * 9 + 1000ms) after the first click.
I think you're better off removing the else case altogether, so that on click it will fire the function if it has not been called within the last second, otherwise it will do nothing with no attempt to queue another call up for later. Not only does that seem to me like a more logical way to operate, it halves the size of your function...
On the other hand, since you mentioned Ajax, rather than disabling the function based on a timer you may like to disable the function until the last Ajax request returns, i.e., use a flag similar to your timerid and reset it within an Ajax complete callback (noting that Ajax complete callbacks get called after success or failure of the request).
In the case of an auto-complete or auto-search function, where you want to send an Ajax request as the user types, you might want to remove the if case from your existing code and keep the else case, because for auto-complete you likely want to wait until after the user stops typing before sending the request - for that purpose I'd probably go with a shorter delay though, say 400 or 500ms.
Regarding general structure of the code, if I wanted a function to be fired a maximum of once per second I'd likely put that control into the function itself rather than in a click handler:
var myFunction = function() {
var timerid = null;
return function() {
if (timerid) return;
timerid = setTimeout(function(){ timerid=null; }, 1000);
// actual work of the function to be done here
console.log("myFunction fired");
};
}();
$("#foo").click(function() {
myFunction();
});
The immediately invoked anonymous function that I've added makes it uglier, but it keeps the timerid variable out of the global scope. If you don't like that obviously you could simply declare timerid in the same scope as myFunction() as you currently do.
This answer is getting kind of long, but if you have a lot of different functions that all need some kind of repeat control in them you could implement a single function to handle that part of it:
function limitRepeats(fn, delay) {
var timerid = null;
return function() {
if (timerid) return;
timerid = setTimeout(function(){ timerid = null; }, delay);
fn();
};
}
// myFunction1 can only be called once every 1000ms
var myFunction1 = limitRepeats(function() {
console.log("fired myFunction1()");
}, 1000);
// myFunction2 can only be called once every 3000ms
var myFunction2 = limitRepeats(function() {
console.log("fired myFunction2()");
}, 3000);
$("#foo").click(function() {
myFunction1();
myFunction2();
});

Categories