-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix listenTo memory leak #3455
Fix listenTo memory leak #3455
Conversation
@@ -79,7 +79,7 @@ | |||
test("listenTo and stopListening with event maps", 4, function() { | |||
var a = _.extend({}, Backbone.Events); | |||
var b = _.extend({}, Backbone.Events); | |||
var cb = function(){ ok(true); }; | |||
var cb = function(){ console.log('called'); ok(true); }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😄
944787f
to
f122de6
Compare
da8e79b
to
eb28f9a
Compare
If you trigger with an object, it's prop value is prepended onto the listener's arguments.
As a side: I have a completed |
Cleaning up the duplication would be lovely. |
Ok, it's included now. |
To be clear, I think the trigger with object behavior is silly. But I needed tests to ensure that my [`eventsApi` refactor] (https://github.com/jridgewell/backbone/tree/eventsApi-refactor) kept the current functionality.
6ad0063
to
59c3704
Compare
// Maps the normalized event callbacks into onceWrappers. | ||
var onceMap = function(name, callback, offer) { | ||
return eventsApi(function(map, name, callback, offer) { | ||
if (callback) map[name] = onceWrap(name, callback, offer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to inline onceWrap
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. I'll fix it.
Ping @akre54: I commented, and added a |
if (events) triggerEvents(events, args); | ||
if (allEvents) triggerEvents(allEvents, arguments); | ||
|
||
// Use `eventsApi` to normalize `name` into the proper event names. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mind clarifying this? I'm not sure what this is getting at.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jridgewell the comments are still lacking. Can you do a pass to make sure the logic is clear? I'm finding it hard to follow what is happening and why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't revised the comments yet, was trying to solve the off
problem first.
Is there a particular spot you're not able to follow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line in particular isn't clear what it does, but the bits with triggerSentinel
and the numerous internal functions mostly.
It almost seems like we should drop the Events = {on: ..., off: ..., listenTo: ...}
object, and just colocate the different events methods (Events.on = ...; onApi = ...; Events.off = ...;
.) To follow this logic, I'm having to jump back and forth through the source to even understand where the different pieces are going.
There's so much extra overhead and indirection going on here, it really detracts from the readability of the Events code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree the source could be better arranged, I'll work on that tonight along with the comments.
There's so much extra overhead and indirection going on here
I actually thought it was kind of easier to follow without the old eventsApi
behavior, but then I had to account for off
cleaning up any listeners. It makes it easier in my mind to think of all the *Api
functions as special purpose reduce iteratees, with eventsApi
being _.reduce
.
I think a little more of the "why" and the "how" and less "what" in the comments might be helpful. Maybe a sentence or two at the top briefly describing how the many abstracted functions work. Right now I'm on the fence that this consolidation is worth the added indirection. It definitely makes it harder to follow the logic. Can you also make a jsperf showing the extra functions aren't too horrible and squash to a single commit and I'll merge? Thanks for your hard work on this. (And no stress about getting it done today... enjoy the snowday!) |
Here's a simple jsperf: http://jsperf.com/eventsapi-refactor (It was hidden in 50adef3's commit message). Safari seems to be the only one taking a hit, but it's still very fast. |
how about vs master? That's showing all the changes with the extra On Tue Jan 27 2015 at 10:42:24 AM Justin Ridgewell notifications@github.com
|
Here's the simple case against master, and another using space-separated events. I did the original tests against the |
Actually, the performance hit seems to be coming from #3463, which is also merged into |
Nice, but does not seem to compete with #3049 as it does not address the following: var a = _.extend({}, Backbone.Events);
var b = _.extend({}, Backbone.Events);
var c = _.extend({}, Backbone.Events);
a.listenTo(c, 'x', fn);
b.listenTo(c, 'x', fn);
c.off('x');
c = null; //c can't be garbage collected until 'stopListening()' is called on a and b Here are the relevant tests test("off removes object from listeners", 6, function() {
var a = _.extend({}, Backbone.Events);
var b = _.extend({}, Backbone.Events);
var c = _.extend({}, Backbone.Events);
var d = _.extend({}, Backbone.Events);
var fn = function() {};
b.listenTo(a, 'all', fn);
c.listenTo(a, 'all', fn);
c.listenTo(d, 'x', fn);
equal(_.size(b._listeningTo), 1);
equal(_.size(c._listeningTo), 2);
equal(_.size(a._listeners), 2);
a.off();
equal(_.size(b._listeningTo), 0);
equal(_.size(c._listeningTo), 1);
equal(_.size(a._listeners), 0);
});
test("off w/event name removes object from listeners", 8, function() {
var a = _.extend({}, Backbone.Events);
var b = _.extend({}, Backbone.Events);
var c = _.extend({}, Backbone.Events);
var fn = function() {};
b.listenTo(a, 'x', fn);
c.listenTo(a, 'x y', fn);
equal(_.size(b._listeningTo), 1);
equal(_.size(c._listeningTo), 1);
equal(_.size(a._listeners), 2);
a.off('x');
equal(_.size(b._listeningTo), 0);
equal(_.size(c._listeningTo), 1);
equal(_.size(a._listeners), 1);
a.off('y');
equal(_.size(c._listeningTo), 0);
equal(_.size(a._listeners), 0);
}); |
ad3e065
to
c7309a3
Compare
I've addressed @smelnikov's |
c7309a3
to
f28fca1
Compare
This is to prevent an infinite off -> stopListening -> off -> stopListening.
f28fca1
to
826b110
Compare
var ids = context != null ? [context._listenId] : _.keys(listeners); | ||
for (var i = 0, length = ids.length; i < length; i++) { | ||
var listener = listeners[ids[i]]; | ||
if (!listener) break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this necessary? I would expect a continue
if anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you listenTo
, context is forced to be this
. If we're off
ing, we need to consider two circumstances:
- No context
- We'll loop through all our listeners, since any could be affected.
- This conditional will never be true, so we'll never
break
.
- Context
- Only loop through the listeners of the corresponding
_listenId
, since only that listener will have the samecontext
- If we have
listeners
, but not that_listenId
, thenbreak
.ids
is guaranteed to only equal[context._listenId]
, so there's no usecontinue
ing.
- If we have that
_listenId
, all is good.
- Only loop through the listeners of the corresponding
@akre54 Commented and rearranged. |
I think let's maybe merge this for now -- and then go over it and tweak as need be. (But hopefully need not ;) |
We should eventually pull in their changes but for a hope in actually getting this fix in production I opted to do this instead for now.
We should eventually pull in their changes but for a hope in actually getting this fix in production I opted to do this instead for now.
Fixes #3453, and competes with #3049.
I think this aligns more closely with what's currently implemented. 😄
It also implements
listenApi
, which iseventsApi
translated to thelistenTo
/stopListening
method signatures.