Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly debounce ajax requests

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?

like image 996
Gandalf StormCrow Avatar asked May 06 '14 11:05

Gandalf StormCrow


2 Answers

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.

like image 100
Gherman Avatar answered Sep 17 '22 07:09

Gherman


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.

like image 45
phuzi Avatar answered Sep 17 '22 07:09

phuzi