Skip to content

Instantly share code, notes, and snippets.

@karlwestin
Created July 10, 2014 10:25
Show Gist options
  • Save karlwestin/6df8aad605e164cf5acb to your computer and use it in GitHub Desktop.
Save karlwestin/6df8aad605e164cf5acb to your computer and use it in GitHub Desktop.
Causing debounced handlers to be triggered with Window as context instead of 'this'
/*
reference: https://gist.github.com/karlwestin/a3a26d3a100f58cf4c5e
code below looks pretty innocent, on every event,
we're calling our 'myHandler', after events finishes triggering.
we want myHandler to be called on the 'thing' object itself.
*/
var thing = _.extend({}, Backbone.Events, {
myHandler: function() {
console.log("MyHandler called", this);
this.trigger("thing");
},
initialize: function() {
this.on("all", _.debounce(this.myHandler), this);
}
});
thing.initialize();
thing.trigger("hi");
/*
However, when the debounced handler is executed, we trigger a new event on the same object.
This causes ANOTHER execution of 'myHandler' to be scheduled. And we see this time,
myHandler is not executed on 'this' but on 'window'
Let's look at the _.debounce code:
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = _.now() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate && !timeout;
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
Look at this thing:
result = func.apply(context, args);
context = args = null;
Line 1. calls our handler (and since it triggers a new event, schedules a new call on the debounced function, with the right context)
Line 2. Deletes the context for the call that was scheduled on line 1.
Solution seems to be: don't trigger events in debounced handlers
Or should we submit a PR to underscore that saves the args separately, then nullifies context and then executes handler?
*/
@karlwestin
Copy link
Author

So to answer the Q: is it my fault or an underscore bug?
It seems to be my fault, cause triggering events in a debounced function that schedules a new exec of the debounced func could create infinite loops. It's a pretty subtle thing though

@karlwestin
Copy link
Author

That means you could fix the bug by changing the _.debounce to:

  _.debounce = function(func, wait, immediate) {
    var  timeout, args, context, timestamp, result;

    var later = function() {
      var last = _.now() - timestamp,
          c, a;
      if (last < wait) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        if (!immediate) {
          c = context;
          a = args;
          context = args = null;
          result = func.apply(c, a);
          c = a = null;
        }
      }
    };

    return function() {
      var c, a;
      context = this;
      args = arguments;
      timestamp = _.now();
      var callNow = immediate && !timeout;
      if (!timeout) {
        timeout = setTimeout(later, wait);
      }
      if (callNow) {
        c = context;
        a = args;
        context = args = null;
        result = func.apply(c, a);
        c = a = null;
      }

      return result;
    };
  };

But that creates an infinite loop out of the example in the gist

I wonder if there could be some way of protecting debounced functions from triggering themselves?

@marijnh
Copy link

marijnh commented Jul 10, 2014

Nowhere in the underscore docs does it say that _.debounce does anything with the context it receives, so I think this is your bug for passing it this.myHandler, which is simply a function, rather than this.myHandler.bind(this).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment