Yesterday I came across a very timely post by John Hann on Debouncing Javascript Methods. I say timing, because this was a problem I was just getting ready to solve again for umpteenth time—managing rapidly fired events in JavaScript.
I was working on some live search functionality for a page where I was searching DOM elements and populating a list of matching elements. Since this was a "live" search, I'm doing the update as the user types. The trick to this issue is that you don't really want to fire off the process each time the user presses a key, but instead you want to really fire it off when there's been a delay/pause to their typing. If you actually do the processing on each character, many of the calls you're making become quickly invalidating as the input changes so quickly that you end up making unnecessary calls. You'll also see a noticeable slow down in performance if your process is CPU intensive.
You can run into the same issue when dealing with AJAX operations. If you're just updating some portion of the screen based upon some user interaction, you really only want to fire off the AJAX call when the user is done interacting with the element.
This is where John's debounce solution comes into play.
In the past, I've always rolled up some solution manually and never came up with a particularly good re-usable solution (which John's has done with the debounce functions.) The idea John's using is essentially the same one I've used—fire off an async event using setTimeout() with a short delay which is cancelled if there's another request to the same function within the delay. What this does is make sure we only run the function when it really matters. This means if we fire off a function 10 times in 1 second, we only ever end up triggering the logic the last time we call the function (because the first 9 iterations become outdated so quickly, there's no point in run the process.)
What John has done is come up with a re-usable function that handle all the heavy lifting for you. No longer do you need to worry about handling this logic within your function, the debounce() function handles it for you.
Let's look at John's code:
Function.prototype.debounce = function (threshold, execAsap) { var func = this, // reference to original function timeout; // handle to setTimeout async task (detection period) // return the new debounced function which executes the original function only once // until the detection period expires return function debounced () { var obj = this, // reference to original context object args = arguments; // arguments at execution time // this is the detection function. it will be executed if/when the threshold expires function delayed () { // if we're executing at the end of the detection period if (!execAsap) func.apply(obj, args); // execute now // clear timeout handle timeout = null; }; // stop any current detection period if (timeout) clearTimeout(timeout); // otherwise, if we're not already waiting and we're executing at the beginning of the detection period else if (execAsap) func.apply(obj, args); // execute now // reset the detection period timeout = setTimeout(delayed, threshold || 100); }; }
While the function itself isn't very big, the syntax may look confusing if you're not well versed in JavaScript. In a nutshell, what happens is your original function is replaced by a copy of the function that is debounced. So, if the function is called multiples times within the threshold value, then only the calls that occur outside of that threshold are actually processed.
Since this concept is easier grasps seeing a live example, let's take a look at one.
In the following example, we will monitor each key press and output the value. Here's the source code we're using:
document.getElementById("ex1").onkeypress = function (){ // run update keyTest("ex1"); };
Now, trying typing in the box below. You'll notice that every keystroke you press registers an update to the screen.
Type:
Now let's take a look at the same example utilizing the debounce technique. Here's the source code:
document.getElementById("ex2").onkeypress = function (){ // run update keyTest("ex2"); }.debounce(500);
The only key difference is that we've added the debounce() function after the end of the declaration of the function. This works because debounce() was defined as a method of the Function object. The argument value of 250 is telling the debounce function to only run the function if 250ms has passed since the last time the function was called.
Now try typing in the box below. You'll notice that updates only appear when there's a pause in your typing. If you are typing more than 1 character a second, then the updates will not appear until you stop typing.
Type:
NOTE:While I've used 500ms for the debounce threshold, in my testing 250ms appears to be a better value for keyboard related events.
As you can see, the behavior in the debounce() version offers behavior that much more conducive to what we need—especially if you're dealing with AJAX-based operation. If you're firing off the event after each keystroke, you're just performing operations that are invalidated almost immediately and will only slow down performance of your application.
The debouce technique can really be used for lots of things besides just monitoring keystrokes on an element—you could use it to monitor mouse movement or to even handle mouse clicks. It comes in handy any time you really only want to trigger an operation once activity has settled down.
So thanks go to John Hann for such a handy little function!
9 Comments
Comments for this entry have been disabled.