Wednesday, January 25, 2017

Javascript debounce function

Often we need to attach functions on Javascript events, but we need them to not be executed too often. Mouse move or scroll events can fire several times a second and executing some heavy computation directly would make everything slow and unresponsive. That's why we use a method called debounce, that takes your desired function and returns another function that will only get executed so many times in a time interval.



It has reached a certain kind of symmetry, so I like it this way. Let me explain how I got to it.

First of all, there is often a similar function used for debouncing. Everyone remembers it and codes it off the top of the head, but it is partly wrong. It looks like this:
function debounce(fn, wait) {
    var timeout=null;
    return function() {
        var context=this;
        var args=arguments;
        var f=function(){ fn.apply(context,args); };
        clearTimeout(timeout);
        timeout=setTimeout(f,wait);
    };
}
It seems OK, right? Just extend the time until the function gets executed. Well, the problem is that the first time you call the function you will have to wait before you see any result. If the function is called more often than the wait period, your code will never get executed. That is why Google shows this page as *the* debounce reference: JavaScript Debounce Function. And it works, but good luck trying to understand its flow so completely that you can code it from memory. My problem was with the callNow variable, as well as the rarity of cases when I would need to not call the function immediately the first time, thus making the immediate variable redundant.

So I started writing my own code. And it looked like the "casual" debounce function, with an if block added. If the timeout is already set, then just reset it; that's the expected behavior. When isn't this the expected behavior? When calling it the first time or after a long period of inactivity. In other words when the timeout is not set. And the code looked like this:
function debounce(fn, wait) {
    var timeout=null;
    return function() {
        var context=this;
        var args=arguments;
        var f=function(){ fn.apply(context,args); };
        if (timeout) {
            clearTimeout(timeout);
            timeout=setTimeout(f,wait);
        } else {
            timeout=setTimeout(function() {
                clearTimeout(timeout);
                timeout=null;
            });
            f();
        }
    };
}

The breakthrough came with the idea to use the timeout anyway, but with an empty function, meaning that the first time it is called, the function will execute your code immediately, but also "occupy" the timeout with an empty function. Next time it is called, the timeout is set, so it will be cleared and reset with a timeout using your initial code. If the interval elapses, then the timeout simply gets cleared anyway and next time the call of the function will be immediate. If we abstract the clearing of timeout and the setting of timeout in the functions c and t, respectively, we get the code you saw at the beginning of the post. Note that many people using setTimeout/clearTimeout are in the scenario in which they set the timeout immediately after they clear it. This is not always the case. clearTimeout is a function that just stops a timer, it does not change the value of the timeout variable. That's why, in the cases when you want to just clear the timer, I recommend also setting the timeout variable to null or 0.

For the people wanting to look cool, try this version:
function debounce(fn, wait) {
    var timeout=null;
    var c=function(){ clearTimeout(timeout); timeout=null; };
    var t=function(fn){ timeout=setTimeout(fn,wait); };
    return function() {
        var context=this;
        var args=arguments;
        var f=function(){ fn.apply(context,args); };
        timeout
            ? c()||t(f)
            : t(c)||f();
    }
}

Now, doesn't this look sharp? The symmetry is now obvious. Based on the timeout, you either clear it immediately and time out the function or you time out the clearing and execute the function immediately.

Update 26 Apr 2017: Here is an ES6 version of the function:
function debounce(fn, wait) {
    let timeout=null;
    const c=()=>{ clearTimeout(timeout); timeout=null; };
    const t=fn=>{ timeout=setTimeout(fn,wait); };
    return ()=>{
        const context=this;
        const args=arguments;
        let f=()=>{ fn.apply(context,args); };
        timeout
            ? c()||t(f)
            : t(c)||f();
    }
}

0 comments: