10

I have a checkbox than can toggle certain behaviour, however if someone makes a 100 consecutive clicks I don't want to send 100 requests to my server side.

This is what I got in place so far (found this code snippet):

deBouncer = function($,cf,of, interval){
    var debounce = function (func, threshold, execAsap) {
        var timeout;
        return function debounced () {
          var obj = this, args = arguments;
          function delayed () {
            if (!execAsap)
                func.apply(obj, args);
            timeout = null;
          }
          if (timeout)
            clearTimeout(timeout);  
          else if (execAsap)
            func.apply(obj, args);
          timeout = setTimeout(delayed, threshold || interval);
        }
    }
    jQuery.fn[cf] = function(fn){  return fn ? this.bind(of, debounce(fn)) : this.trigger(cf); };
  };

In my document ready function :

deBouncer(jQuery,'smartoggle', 'click', 1500);

Then the event itself :

$(window).smartoggle(function(e){
  MyToggleFunction();
});

This works as I've put 1500 ms to be the debouncing period, so if you click n times withing 1500 ms it will send only the latest state to the server.

There is however side effect of using this, now my click event for other stuff is messed up. Am I doing something wrong here? Is there a better way to debounce?

Gandalf StormCrow
  • 25,788
  • 70
  • 174
  • 263

3 Answers3

14

Just debounce the function that does the actual work and I wouldn't load an entire library for this.

var debouncedSomeFunction = debounce(someFunction, 1500);

debouncedSomeFunction();
debouncedSomeFunction();
debouncedSomeFunction();

setTimeout(debouncedSomeFunction, 2000);


function debounce(fn, bufferInterval) {
  var timeout;

  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(fn.apply.bind(fn, this, arguments), bufferInterval);
  };

}

function someFunction() {
    log('someFunction executed');
}

function log(text) {
   document.body.appendChild(document.createTextNode(text));
   document.body.appendChild(document.createElement('br'));
}
plalx
  • 42,889
  • 6
  • 74
  • 90
  • I know this is pseudo-code but I'm really confused about what the last line means with it's self-reference. Maybe you could update your answer showing a simple implementation of *yourFunction*? I tried this code and couldn't get it to work. – geoidesic May 15 '16 at 21:20
  • Just for clarity, in case someone else was confused by the last line... should be *var yourFunction = function(){}; yourFunction = debounce(yourFunction, 1500); – geoidesic May 15 '16 at 21:28
  • @geoidesic I modified the code, hopefully this is easier to understand. – plalx May 15 '16 at 22:53
  • Please can you explain the `fn.apply.bind` usage in this case? The `bind` will return a new function based on `apply` with the `this` context set to the `fn` function itself? I see it is allowing the returned "debounced" function to be called with the original function's arguments signature. I just can't quite follow how/why :) – El Ronnoco Sep 01 '22 at 09:37
4

Not sure if there can be a "proper" way to do this.

Having said that underscore has such a utility that will create a debounced version of your function...

var MyToggleDebounced = _.debounce(MyToggleFunction, 1500);

then use MyToggleDebounced in your click handler.

Link to debounce docs on underscorejs

Take a look at the annotated source for how they do it.

phuzi
  • 12,078
  • 3
  • 26
  • 50
2

I think that this question is better than it seems first. There is a caveat in how Http Ajax requests work. If you set the delay to 1500ms and you can guarantee that each request is served under this time span than other answers will work just fine. However if any request gets significantly slow it then the requests may come out of order. If that happens, the last processed request is the one for which data will be displayed, not the last sent.

I wrote this class to avoid this caveat (in Typescript, but you should be able to read it):

export class AjaxSync {

  private isLoading: boolean = false;
  private pendingCallback: Function;
  private timeout;

  public debounce(time: number, callback: Function): Function {
    return this.wrapInTimeout(
      time,
      () => {
        if (this.isLoading) {
          this.pendingCallback = callback;
          return;
        }
        this.isLoading = true;
        callback()
          .then(() => {
            this.isLoading = false;
            if (this.pendingCallback) {
              const pendingCallback = this.pendingCallback;
              this.pendingCallback = null;
              this.debounce(time, pendingCallback);
            }
          });
      }
    );
  }

  private wrapInTimeout(time, callback) {
    return () => {
      clearTimeout(this.timeout);
      this.timeout = setTimeout(callback, time);
    };
  }
}

This will prevent two ajax-requests processed at the same time and this will send another request if there is a pending one.

Gherman
  • 6,768
  • 10
  • 48
  • 75