Created
July 10, 2014 10:25
-
-
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'
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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? | |
*/ |
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?
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
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