I'm struggling with getting the concept of memory management with single page applications. This is my code:
var FilterModel = Backbone.Model.extend({});
var taskView = Backbone.View.extend({
template: _.template('<h1><%= title %></h1>'),
initialize: function(){
this.render();
this.listenTo(this.model, 'destroy', this.remove);
console.log(this.model)
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
},
events:{
'click h1': 'removeView'
},
removeView: function(){
this.model.destroy();
console.log('removed');
}
});
var filterModel = new FilterModel({title: 'Test'});
var taskview = new taskView({model:filterModel});
// I make heap snapshot before and after the change!
setTimeout(function(){
$("h1").click()}, 3000
)
$('body').append(taskview.$el);
I was told by numerous articles that using "remove" and "destroy" would clean up any memory leaks when removing the DOM tree.
But Chrome profile utility tells otherwise. I get detached DOM elements no matter what I do.
UPDATE!!!
After trying a few things in the responses I still get this in Google Chrome:
Here is jsfiddle: http://jsfiddle.net/HUVHX/
taskview is still holding a strong reference to this.el, although it is not connected to the dom. This is not a memory leak because taskview is held strongly also by it's variable
To test my assumption just add:
removeView: function(){
this.model.destroy();
this.el = undefined;
this.$el = undefined;
}
Another approach is to undef taskview var
EDIT:
When I change: "click h1" : "removeView" To "click": "removeView" it solves the detached dom node leak.
I suspect this has something to do with jquery selector caching.
You can see in backbone code, the difference is in calling jquery on function with a selector:
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
I tried to trace the cache deep into jquery code, with no luck.
So Janck, you can fin your answer here:
Backbone remove view and DOM nodes
The problems is that you have to do more than just remove you model and view.
You need to properly destroy all of the events and other bindings that are hanging around when you try to close your views.
I don't know if you know about Marionette.js (Backbone.Marionette), but it's a great extension to Backbone to handle this Zombie Views and to create robust JS applications.
You can read some articles about this as well, they were pointed in the Stackoverflow link that I posted.
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
http://lostechies.com/derickbailey/2012/03/19/backbone-js-and-javascript-garbage-collection/
But the logic is this: If a View is listening a model, then the contrary also occurs, so you'll always get a instance of your View in your DOM.
Related
I am working on a Backbone demo app that shows a list of tweets. As I am replacing all the "tweets" with different data I clear the list using $.html()
render: function() {
$("#item-table").html('');
this.collection.each(this.addItem);
}
I was wondering if anyone could give me a hint with what can I replace this $.html() for better performance, because by using $.html() I am causing reflows and which gives bad layout process times.
There are two other places in the code where I use $.html() and it would be really great if someone could give me advice on how to change those too if those other places are even possible.
Create a new DocumentFragment to pre-render all the items, then update the DOM once.
Also, favor this.$(...) over the global jQuery selector $(...).
this.$ is a proxy to this.$el.find(...) which is more efficient, and less prone to select something outside of the view.
Using jQuery's core function ($()) inside a view can fail if the view wasn't rendered yet. So it's better to always manipulate through this.$el so you can make changes even before the view is actually put in the DOM.
Keep all the sub views created in an array to cleanly remove them later.
initialize: function() {
this.childViews = [];
},
render: function() {
// cache the list jQuery object
this.$list = this.$("#item-table");
// Make sure to destroy every child view explicitely
// to avoid memory leaks
this.cleanup();
this.renderCollection();
return this;
},
The real optimization starts here, with a temporary container.
renderCollection: function() {
var container = document.createDocumentFragment();
this.collection.each(function(model) {
// this appends to a in memory document
container.appendChild(this.renderItem(model, false).el);
}, this);
// Update the DOM only once every child view was rendered.
this.$list.html(container);
return this;
},
Our renderItem function can still be used to render a single item view and immediatly put it in the DOM. But it also provides an option to postpone the DOM manipulation and it just returns the view.
renderItem: function(model, render) {
var view = new Item({ model: model });
this.childViews.push(view);
view.render();
if (render !== false) this.$list.append(view.el);
return view;
},
To avoid memory leaks with dangling listeners, it's important to call remove on each view before forgetting about it.
I use an additional optimization by deferring the actual call to remove so we don't waste time now while the user waits.
cleanup: function() {
var _childViewsDump = [].concat(this.childViews);
this.childViews = [];
while (_childViewsDump.length > 0) {
var currentView = _childViewsDump.shift();
// defer the removal as it's less important for now than rendering.
_.defer(currentView.remove.bind(currentView), options);
}
}
I am having issues at solving backbone memory leaks, so far I attempted to clean the views using jquery CleanData when the views are removing from the DOM.
Each time a view is removed (even via jQuery .html()) it hit the dispose() method that should theoretically kill all references that stop the view from being collected. Unfortunately the app just build up memory
code below,
Backbone.View.prototype.dispose = function(){
// unbind all events, so we don't have references to child views
this.unbind();
this.off();
// if we have a collection - unbind all events bound to this context
this.collection && this.collection.off(null, null, this);
// do the same with a model
this.model && this.model.off(null, null, this);
delete this;
};
clean data :
$.cleanData = function( elems ) {
$.each( elems, function( i, elem ) {
if ( elem ) {
$(elem).trigger("dispose");
}
});
oldClean(elems);
};
The code work, dispose is hit (I added the event in the view) but it's never collected anyway when I change page.
(About the dispose event..)
I do not explicitly call remove on all views. the app container is emptied, jQuery does cleanData. I added an event dispose & I trigger that func in cleandata
I think the problem is delete this doesn't do what you thought it would. It depends on how you initiate your view. Do you assign your view to any variable or initiate it in a scope that live beyond the point where you change your page?
Also, there is a function remove() on Backbone View
More on JavaScript delete http://perfectionkills.com/understanding-delete/ and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete
I have been playing with backbone and trying to learn it. I'm stuck at this point for a while. Not able to figure out what's wrong with following code?
render: function() {
this.$el.empty();
// render each subview, appending to our root element
_.each(this._views, function(sub_view) {
this.$el.append(sub_view.render().el); // Error on this line
});
You have got context issue. this you are refering to doesn't contain the $el you are looking for. You can fix this by declaring a self variable that points to appropriate this. Following code should work for you.
render: function() {
var self = this; //Added this line to declare variable (self) that point to 'this'
this.$el.empty();
_.each(this._views, function(sub_view) {
self.$el.append(sub_view.render().el); //Used 'self' here instead 'this'
});
Side Note: As you are leaning backbone also you should know about a very commong JavaScript problem with document reflow. You are render a view for every single model in the collection. It can lead to performance issues and especially on old computers and mobile devices. You can optimise your code by rendering everything in container and appending it once, rather than updating DOM each time. Here is an example:
render: function() {
this.$el.empty();
var container = document.createDocumentFragment();
_.each(this._views, function(sub_view) {
container.appendChild(sub_view.render().el)
});
this.$el.append(container);
}
I have a sample single-page-application in Backbone that I am messing around with. The idea is that I have a button that will trigger a refresh on a view. I am trying to get the event handler to remember 'this' as the backbone view, not the element that it was called on. No matter how much docs I read, I cant seem to make it past this mental hump.
in my view, I have
initialize: function() {'
//i am trying to say, call render on this view when the button is clicked. I have tried all of these calls below.
//this.$("#add-tweet-button").click(this.render);
//this.$("#add-button").bind("click", this.render);
}
When the render function is called, the 'this' element is the button. I know what im missing is pretty easy, can someone help me out with it? Also, is this sound as coding conventions go?
If you use the View's 'delegateEvents' functionality, the scoping is taken care of for you:
var yourView = Backbone.View.extend({
events: {
"click #add-tweet-button" : "render"
},
render: function() {
// your render function
return this;
}
});
This only works with elements that are 'under' the View's El. But, your example shows this.$(...), so I'm assuming this is the case.
#Edward M Smith is right, although if you need to handle a jquery event of element outside the scope of your View you might write it that way :
var yourView = Backbone.View.extend({
initialize: function() {
var self = this;
$("button").click(function () {
self.render.apply(self, arguments);
});
},
render: function(event) {
// this is your View here...
}
});
I'm trying to make a preloader and getting caught at step one with backbone. I've built a nice one before using jquery and also with 'raw' js basically what happens is that I have a folder called img/ui and a server side script that just gives a JSON dump of that folder. This request is /preload the js then queues this and loads them one by one based upon a process of load events with timeouts & error handlers.
What I'm trying to is port this to Backbone. The pattern I thought was a collection which loads the JSON build a set of models for each of the assets then a single view attached to the collection to display the status of the queue...... simple.
But I'm already stuck.. first I have to manually fetch the JSON or it wont do anything.. fine.. done, second even when the JSON is loaded it wont fire the parse method (or any other):
var PreloaderCollection = Backbone.Collection.extend({
model:PreloaderModel,
url:"/preload",
events: {
"change":"parse"
},
initialize: function()
{
_.bindAll(this);
this.fetch(this.url);
},
setup: function(args)
{
},
update: function(args)
{
},
parse:function(args){
log(args)
},
remove: function(args)
{
}
});
I'm really starting to get frustrated with Backbone, It's my first major project and despite reading about every tutorial and fully going through the source there seems to be so much contradiction about the pattern and capabilities.
EDIT:
This was a last resort and feels very dirty but here's how I 'bypassed' the issue.
I essentially overrode the fetch function with my own like so, which now works but... hmm,
var PreloaderCollection = Backbone.Collection.extend({
url:"/preload",
events: {
"reset":"parse"
},
initialize: function()
{
log("initing preloader collection")
_.bindAll(this);
this.bind("change",this.parse)
this.fetch(this.url);
},
fetch:function(args){
$.getJSON(
args,
this.parse
)
},
setup: function(args)
{
},
update: function(args)
{
},
parse:function(args){
log("parse",args)
},
remove: function(args)
{
}
});
you are using the wrong way of binding to your events :)
whith the event's hash, you declare all events jquery need to bind to elements in the DOM, on your view.
in a model, or collection you bind to reset / change / error events like this:
var myModel = Backbone.Model.extend({});
var myCollection = Backbone.Collection.extend({
model: myModel,
initialize: function() {
this.bind('reset', this.parse, this)
},
parse: function() {
alert('parsing');
}
});
var c = new myCollection({});
c.reset([{name: 'name1'}, {name: 'name2'}]);
see more info on eventbinding here: http://documentcloud.github.com/backbone/#Events-bind
see more info on the delegateEvents you were trying to use, but are only meant to be used in a view for DOM element event binding: http://documentcloud.github.com/backbone/#View-delegateEvents
see jsfiddle for a working version: http://jsfiddle.net/saelfaer/kt2KJ/1/
The event to listen for after fetching is reset. So for actin on that you will have to write:
events: {
"reset":"parse"
}
The documentation for fetch can be found on the backbone page:
http://documentcloud.github.com/backbone/#Collection-fetch