in backbone we have an app that uses an event Aggregator, located on the window.App.Events
now, in many views, we bind to that aggregator, and i manually wrote a destroy function on a view, which handles unbinding from that event aggregator and then removing the view. (instead of directly removing the view).
now, there were certain models where we needed this functionality as well, but i can't figure out how to tackle it.
certain models need to bind to certain events, but maybe i'm mistaken but if we delete a model from a collection it stays in memory due to these bindings to the event aggregator which are still in place.
there isn't really a remove function on a model, like a view has. so how would i tacke this?
EDIT on request, some code example.
App = {
Events: _.extend({}, Backbone.Events)
};
var User = Backbone.Model.extend({
initialize: function(){
_.bindAll(this, 'hide');
App.Events.bind('burglar-enters-the-building', this.hide);
},
hide: function(burglarName){
this.set({'isHidden': true});
console.warn("%s is hiding... because %s entered the house", this.get('name'), burglarName);
}
});
var Users = Backbone.Collection.extend({
model: User
});
var House = Backbone.Model.extend({
initialize: function(){
this.set({'inhabitants': new Users()});
},
evacuate: function(){
this.get('inhabitants').reset();
}
});
$(function(){
var myHouse = new House({});
myHouse.get('inhabitants').reset([{id: 1, name: 'John'}, {id: 1, name: 'Jane'}]);
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar1');
myHouse.evacuate();
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar2');
});
view this code in action on jsFiddle (output in the console): /
as you can see, i don't bind to the events on the model, but to an event aggregator. unbinding events from the model itself, is not necessary because if it's removed nobody will ever trigger an event on it again. but the eventAggregator is always in place, for the ease of passing events through the entire app.
the code example shows, that even when they are removed from the collection, they don't live in the house anymore, but still execute the hide mand when a burglar enters the house.
in backbone we have an app that uses an event Aggregator, located on the window.App.Events
now, in many views, we bind to that aggregator, and i manually wrote a destroy function on a view, which handles unbinding from that event aggregator and then removing the view. (instead of directly removing the view).
now, there were certain models where we needed this functionality as well, but i can't figure out how to tackle it.
certain models need to bind to certain events, but maybe i'm mistaken but if we delete a model from a collection it stays in memory due to these bindings to the event aggregator which are still in place.
there isn't really a remove function on a model, like a view has. so how would i tacke this?
EDIT on request, some code example.
App = {
Events: _.extend({}, Backbone.Events)
};
var User = Backbone.Model.extend({
initialize: function(){
_.bindAll(this, 'hide');
App.Events.bind('burglar-enters-the-building', this.hide);
},
hide: function(burglarName){
this.set({'isHidden': true});
console.warn("%s is hiding... because %s entered the house", this.get('name'), burglarName);
}
});
var Users = Backbone.Collection.extend({
model: User
});
var House = Backbone.Model.extend({
initialize: function(){
this.set({'inhabitants': new Users()});
},
evacuate: function(){
this.get('inhabitants').reset();
}
});
$(function(){
var myHouse = new House({});
myHouse.get('inhabitants').reset([{id: 1, name: 'John'}, {id: 1, name: 'Jane'}]);
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar1');
myHouse.evacuate();
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar2');
});
view this code in action on jsFiddle (output in the console): http://jsfiddle/saelfaer/szvFY/1/
as you can see, i don't bind to the events on the model, but to an event aggregator. unbinding events from the model itself, is not necessary because if it's removed nobody will ever trigger an event on it again. but the eventAggregator is always in place, for the ease of passing events through the entire app.
the code example shows, that even when they are removed from the collection, they don't live in the house anymore, but still execute the hide mand when a burglar enters the house.
Share Improve this question edited May 3, 2012 at 11:42 Sander asked May 3, 2012 at 10:36 SanderSander 13.4k15 gold badges73 silver badges98 bronze badges 6-
For a better understanding of your question would be nice to see a few lines of code with examples of how and where you
bind
the Model events that you want tounbind
when the Model is removed. Seeing the directions of the event-binding is very important. – fguillen Commented May 3, 2012 at 10:59 -
@Sander the problem is called Ghost views. During
model.destroy()
the view should be able to bind todestroy
event. During which the proper events must be undelegated and then the View must be destroyed. – Deeptechtons Commented May 3, 2012 at 11:02 - @Deeptechtons the ghost views are not a problem those are properly cleared, see my exanple, it isn't even working with views at this state, just models and collections, binding to an aggregator. when models are removed, they still exist (bound and executing when the event is called) – Sander Commented May 3, 2012 at 12:29
- I see the binding-event direction is Model -> Aggregator so there is not any reference from Aggregator to Model so you don't have any thing to clean-up after Model is removed. – fguillen Commented May 3, 2012 at 12:49
- Aparently the model is still bound, as you can see in he example, even though he 2 persons are removed from the users collection in the house, they still execute the hide function when the event is thrown. so clearly they are not properly remvoed... and that's what i'm trying to achieve, unbinding them from he aggregator so they free up the memory they use :) – Sander Commented May 3, 2012 at 12:55
3 Answers
Reset to default 13I see that even when the binding event direction is this way Object1 -> listening -> Object2 it has to be removed in order to Object1 lost any alive reference.
And seeing that listening to the Model remove
event is not a solution due it is not called in a Collection.reset()
call then we have two solutions:
1. Overwrite normal Collection cleanUp
As @dira sais here you can overwrite Collection._removeReference
to make a more proper cleaning of the method.
I don't like this solutions for two reasons:
- I don't like to overwrite a method that has to call
super
after it. - I don't like to overwrite private methods
2. Over-wrapping your Collection.reset()
calls
Wich is the opposite: instead of adding deeper functionality, add upper functionality.
Then instead of calling Collection.reset()
directly you can call an implementation that cleanUp the models before been silently removed:
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
A sorter version of your code can looks like this:
AppEvents = {};
_.extend(AppEvents, Backbone.Events)
var User = Backbone.Model.extend({
initialize: function(){
AppEvents.on('my_event', this.listen, this);
},
listen: function(){
console.log("%s still listening...", this.get('name'));
},
unlink: function(){
AppEvents.off( null, null, this );
}
});
var Users = Backbone.Collection.extend({
model: User,
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
});
// testing
var users = new Users([{name: 'John'}]);
console.log('users.size: ', users.size()); // 1
AppEvents.trigger('my_event'); // John still listening...
users.cleanUp();
console.log('users.size: ', users.size()); // 0
AppEvents.trigger('my_event'); // (nothing)
Check the jsFiddle.
Update: Verification that the Model is removed after remove the binding-event link
First thing we verify that Object1 listening to an event in Object2 creates a link in the direction Obect2 -> Object1:
In the above image we see as the Model (@314019) is not only retained by the users
collection but also for the AppEvents
object which is observing. Looks like the event linking for a programmer perspective is Object that listen -> to -> Object that is listened but in fact is pletely the opposite: Object that is listened -> to -> Object that is listening.
Now if we use the Collection.reset()
to empty the Collection we see as the users
link has been removed but the AppEvents
link remains:
The users
link has disappear and also the link OurModel.collection
what I think is part of the Collection._removeReference()
job.
When we use our Collection.cleanUp()
method the object disappear from the memory, I can't make the Chrome.profile
tool to explicitly telling me the object @314019 has been removed but I can see that it is not anymore among the memory objects.
I think the clean references process is a tricky part of Backbone
.
When you remove a Model
from a Collection
the Collection takes care to unbind
any event on the Model that the Collection its self is binding. Check this private Collection method.
Maybe you can use such a same technique in your Aggregator:
// ... Aggregator code
the_model.on( "remove", this.unlinkModel, this );
// ... more Aggregator code
unlinkModel: function( model ){
model.off( null, null, this );
}
This is in the case the direction of the binding is Aggregator -> Model. If the direction is the opposite I don't think you have to make any cleaning after Model removed.
Instead of wrapping Collection
's reset
with cleanUp
as fguillen suggested, I prefer extending Collection
and overriding reset
directly. The reason is that
cleanUp
takes effect only in client's code, but not in library(i.e. Backbone)'s.
For example, Collection.fetch
may internally call Collection.reset
. Unless modifying the Backbone's source code, we cannot unbind models from events(as in cleanUp
) after calling Collection.fetch
.
Basically, my suggested snippet is as follows:
var MyCollection = Backbone.Collection.extend({
reset: function(models, options) {
this.each(function(model) {
model.unlink(); // same as fguillen's code
});
Backbone.Collection.prototype.reset.apply(this, arguments);
}
});
Later, we can create new collections based on MyCollection
.