When does a BackboneJS ModelView get garbage collected in this case? - javascript

I have a ListView and InstanceView defined in my Backbone code. The ListView is associated with a collection and it instantiates an InstanceView like so
render: function () {
this.collection.forEach(function(instance){
var commentHTML = new InstanceView({
model: instance
}).render();
renderedComments.push(commentHTML);
});
}
The new view instance goes out of scope after the render call finishes. What I've noticed is that the view persists in memory, though. I can tell because the events attached to it still fire long after the render method terminates.
So, does the view avoid gc because of its reference to the model object which, in turn, is referenced by the collection?

When the view is created events are registered on the model. The callbacks have references to the view, preventing it from being gc'ed.
Check out the backbone code on github. The events are hooked up in the delegateEventsmethod.

Objects are garbage collected whenever the JS engine feels like collecting them.
Your question, however, is actually different.
Just because you cannot access an object does not mean:
Nothing can, and
Things it attaches also go away.
Also, you explicitly add the object to collection, so it's not eligible for collection.

Related

Memory Management in Backbone.js

Consider the following:
SomeView = Backbone.View.extend({
render0: function () {
var view0 = new View0();
view0.setElement("#right-block");
view0.render();
},
render1: function(event) {
var view1 = new View1();
view1.setElement("#right-block");
view1.render();
},
});
If I call render0() and then render1, what will happen to the object view0? Do I have to explicitly destroy the old view?
Your view0 will stay in memory as long as the DOM element #right-block exists. Because the event handlers on the DOM element is pointing to methods of your view, so it won't be garbage collected.
Ideally you should invoke view0.remove(), which will remove the element from DOM and also calls stopListening().
But in your sample code, if you do that, the element #right-block will be removed from DOM, and view1.setElement("#right-block"); won't work as expected.
In this case try invoking view0.undelegateEvents(); view0.stopListening();, and if nothing else is referring to the view instance, it'll be garbage collected
No, you don't have to destroy the old view. Variables in objects that themselves go out of scope do not have to be manually cleared. When they go out of scope or when the parent object is deleted, the data contained within will also be eligible for garbage collection.

How to ensure proper closing of backbone views

I have a close function attached to all of my views,
Backbone.View.prototype.close = function()
{
this.remove();
this.unbind();
if (this.closeMe) this.closeMe();
};
And in closeMe() function inside views I call off() function of backbone to remove previously-bound callback function from models and collections.
closeMe: function()
{
if(this.model)
this.model.off(null, null, this);
...
}
Question is, If I have some variables attached to current view in initialize function, Do I need to handle them via closeMe() function?
initialize : function(options)
{
...
this.myVar= options.something;
}
The garbage collector do this work for you.
The garbage collector algorithm reduces the definition of "an object is not needed anymore" to "an object has no other object referencing to it". An object is considered garbage collectable if there is zero reference pointing at this object.
When the reference to the view has zero reference pointing at it, the garbage collector will delete this object. At this moment, if the object myVar has no references pointing at it, the garbage collector will delete it.
By the way, I suggest to use the method listenTo to listen events instead of method on because simplify your code. When you call remove on Backbone View, removes a view from the DOM, and calls stopListening over the view to remove any bound events that the view has listenTo'd.

Circular reference memory leak?

I am in doubt if the following design pattern would cause a memory leak.
I have been using it for some time with success, but I haven't seen this pattern used by others, so I'd like some confirmation if you see something wrong with it.
As from next month I have to start working on a large project, and I want to know for sure that I can use this without problems, or if I should use another strategy.
controller.js:
var Controller = function(options){
};
Controller.prototype.makeView = function(options){
options.controller = this;
options.otheroption = options.otheroption;
var view = new View(options);
};
Controller.prototype.getModel = function(options){
//--- Get model ---
var model = new Model();
var promise = model.fetch();
return promise;
});
view.js:
var View = Backbone.View.extend({
initialize: function(options){
this.controller = options.controller;
this.otheroption = options.otheroption;
},
getModel: function(){
var promise = this.controller.getModel();
promise.done(_.bind(function(model){
//Do something with the returned model instance
}, this));
};
});
Instantiate controller, eg. from the router, or another controller:
//--- Instantiate the controller and build the view ---//
var controller = new Controller();
controller.makeView(options)
To me, this doesn't look like a circular reference, because both the controller and view are declared as a local variable.
Yet the instantiated view can access the controller functions, which allows me to isolate the RESTful server interactions via models / collections that the view uses.
For me it would seem as if the only reference remaining would be the view that keeps a reference to the controller object.
What I do afterwards is clean up the view (I destroy the instance and its references when I don't need it anymore.
Your opinion on this pattern is highly appreciated.
My purpose is to isolate creation of views / server interactions in separate controller files: if you see holes in my method and have a better way of doing it, please share.
Thanks.
Short answer: There is no memory leak problem in the code you have posted. The view holds a reference to the controller, but not vice versa. So as long as the controller lives longer than the view, that reference does not keep your objects from being garbage-collected. I don't see a circular reference anywhere in your code.
Longer answer: The pitfalls would be in the code you haven't posted. In particular, any event handlers in your view must be cleaned up properly, otherwise your views never fade into oblivion. But you have said in your question that you clean up your view, so I guess you are aware of that sort of problem.
What controller doing is here looks like a utility to me. Could have been easily managed by a global level singleton. I see some issues in first glance.
Code repetition, assuming you would creating separate Controller for different types of Models and Views, makeView and getModel code needs to be repeated for each controller. If you extending from a BaseController, then you need to pass View and Model Class to getModel and makeView functions.
How do you handle a use-case where you have to use same model in different Views?
makeView and getModel is designed assuming for each makeView there would be a getModel call, in assumed order
I would rather write a utility function which can create and deploy views for me.
var deployView = function(view, config){
//do the view rendering
view.render();
view.$el.appendTo(config.el);
}
var createView = function(config) {
var view;
var viewType = 'model';
if (config.collection || config.Collection) {
viewType = 'collection';
}
if (viewType === 'model') {
if (config.Model) {
config.model = new config.Model(config.modelAttributes);
//fetch if needed
}
} else {
if (config.Collection) {
config.collection = new config.Collection(config.items);
//fetch if needed
}
}
var filteredConfig = _.omit(config, 'Collection', 'Model', 'View');
view = new config.View(filteredConfig);
deployView(view, filteredConfig)
}
JavaScript implementations haven't had a problem with circular references for a long time. (IE6 did have a memory leak from circular references if I recall correctly, which wasn't shared by any other major browser from that period.)
Modern JavaScript implementations perform garbage collection through a "mark and sweep" algorithm. First they scan through your web app's entire memory structure starting from the global object, and mark everything they find. Then they sweep through every object stored in memory and garbage collect anything that wasn't marked. As long as there isn't a reference to your object from the global object or any stored function, it can be garbage collected.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#Mark-and-sweep_algorithm
You're probably thinking of a reference counting-based implementation, which does have issues with memory leaks from circular references. In that implementation as long as one object contained a reference to another, that second object can't be garbage collected. That method was once used in web browsers but not anymore.
Nowadays, most memory leaks are from globally-accessible objects you forget to clean up and accidentally retaining data in function closures (a function that creates another function and passes/saves it somewhere). Since the closure's local variables can be accessed by the function created inside of them, they have to be retained as long as that function exists.
So go ahead and add all the circular references you want. Unless you need to target IE6, your code's fine.

How to delete a backbone model client-side?

What's the best way to delete a model client side? I don't need to worry about removing it server-side. How do I make sure it is removed everywhere, avoiding every gotcha, every zombie binding. I'm looking for a guide to removing and destroying everything and ensuring the model is garbage collected.
Thanks!!
It really depends on what is inside this model. If it is binded to events from other instances - View/Collection/Models, you should remove those event listeners manually, since there is no way to remove all of them at once.
Also, Model.destroy() removes the model from any collections ( backbone documents ) :
Destroy model.destroy([options])
... Triggers a "destroy" event on the model, which will bubble up through any collections that contain it ...
The thing that you might want to do is assign a new destroy method which includes the event triggering and the stuff you want to remove.
destroy: function(options) {
// Any events you wish to switch off ( if you have any )
SomeCollection.off('change', this.changeFn);
Backbone.Model.prototype.destroy.apply(this, options);
}
May be you should also be aware of some patterns for making less garbage from Models :
Don't place your initialized model in a variable ( keep it in the collection );
Make sure you write your code in a way that no events are binded from the Model ( Use views/collections for that );
Keep your model code simple, since models in your app will be most numbered.
I think by following those rules you won't need to worry so much about garbage from your Models.

Backbone.js View removing and unbinding

when my page opens, I call the collection and populate the view:
var pagColl = new pgCollection(e.models);
var pagView = new pgView({collection: pagColl});
Separately (via a Datepicker), I wish to want to populate the same collection with different models and instantiate the view again.
The problem I have is how to close the original pagView and empty the pagColl before I open the new one, as this "ghost view" is creating problems for me. The variables referred to above are local variables? is it that I need to create a global pagColl and reset() this?
well there has been many discussion on this topic actually,
backbone does nothing for you, you will have to do it yourself and this is what you have to take care of:
removing the view (this delegates to jQuery, and jquery removes it from the DOM)
// to be called from inside your view... otherwise its `view.remove();`
this.remove();
this removes the view from the DOM and removes all DOM events bound to it.
removing all backbone events
// to be called from inside the view... otherwise it's `view.unbind();`
this.unbind();
this removes all events bound to the view, if you have a certain event in your view (a button) which delegates to a function that calls this.trigger('myCustomEvent', params);
if you want some idea's on how to implement a system I suggest you read up on Derrick Bailey's blogpost on zombie views: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/.
another option
another option would be to reuse your current view, and have it re-render or append certain items in the view, bound to the collection's reset event
I was facing the same issue. I call the view.undelegateEvents() method.
Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.
I use the stopListening method to solve the problem, usually I don't want to remove the entire view from the DOM.
view.stopListening();
Tell an object to stop listening to events. Either call stopListening
with no arguments to have the object remove all of its registered
callbacks ... or be more precise by telling it to remove just the
events it's listening to on a specific object, or a specific event, or
just a specific callback.
http://backbonejs.org/#Events-stopListening
Here's one alternative I would suggest to use, by using Pub/Sub pattern.
You can set up the events bound to the View, and choose a condition for such events.
For example, PubSub.subscribe("EVENT NAME", EVENT ACTIONS, CONDITION); in the condition function, you can check if the view is still in the DOM.
i.e.
var unsubscribe = function() {
return (this.$el.closest("body").length === 0);
};
PubSub.subscribe("addSomething",_.bind(this.addSomething, this), unsubscribe);
Then, you can invoke pub/sub via PubSub.pub("addSomething"); in other places and not to worry about duplicating actions.
Of course, there are trade-offs, but this way not seems to be that difficult.

Categories