I've been trying to debug my Backbone multi-page app for most of the day now to get rid of 'zombies', but unfortunately to no avail. Before today, I didn't even realize I have a zombie problem. What am I doing wrong?
This is my RegionManager:
var regionManager = (function() {
var currView = null;
var rm = {};
var closeView = function(view) {
if (view && view.close) {
view.close();
}
};
var openView = function(view) {
view.render();
if (view.onShow) {
view.onShow();
}
};
rm.show = function(view) {
closeView(currView);
currView = view;
openView(currView);
};
return rm;
})();
This is my View cleaning up function:
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
if (this.views) {
_.invoke(this.views, 'close');
}
// Unbind any view's events.
this.off();
// Unbind any model and collection events that the view is bound to.
if (this.model) {
this.model.off(null, null, this);
}
if (this.collection) {
this.collection.off(null, null, this);
}
// Clean up the HTML.
this.$el.empty();
};
I tried appending the View el
s directly to the body
and using this.remove();
in the View clean-up function (instead of using a common el: $('#content')
to which I am appending elements, then cleaning up by this.$el.empty()
), but that didn't work either.
It might have something to do with my "global Events":
Backbone.Events.on('letterMouseDown', this.letterMouseDown, this);
But I take care of them with the onClose function:
onClose: function() {
Backbone.Events.off('letterMouseDown');
}
One problem I see is that your close
function never removes the event delegator from the view's el
. A view's events are handled by using the delegator form of jQuery's on
to attach a single event handler to the view's el
. Your close
does:
this.$el.empty();
but that only removes the content and any event handlers attached to that content, it does nothing at all to the handlers attached directly to this.el
. Consider this minimal example:
var V = Backbone.View.extend({
events: {
'click': 'clicked'
},
clicked: function() {
console.log('still here');
}
});
var v = new V({ el: '#el' });
v.close();
After that, clicking on #el
will throw a 'still here'
in the console even though you think that the view has been fully cleaned up. Demo: http://jsfiddle.net/ambiguous/aqdq7pwm/
Adding an undelegateEvents
call to your close
should take care of this problem.
General advice:
Don't use the old-school on
and off
functions for events, use listenTo
and stopListening
instead. listenTo
keeps track of the events on the listener so it is easier to remove them all later.
Simplify your close
to just this:
Backbone.View.prototype.close = function() {
if(this.onClose)
this.onClose();
if(this.views)
_.invoke(this.views, 'close');
this.remove();
};
Don't bind views to existing el
s. Let the view create (and own) its own el
and let the caller place that el
into a container with the usual:
var v = new View();
container.append(v.render().el);
pattern. If you must attach to an existing el
then the view should override remove
with a slightly modified version of the standard implementation:
remove: function() {
this.$el.empty(); // Instead of removing the element.
this.undelegateEvents(); // Manually detach the event delegator.
this.stopListening();
return this;
}
I'm pretty sure I found the root for my problem.
mu is too short was right, with the close()
method I wasn't removing the events bound directly to my el
(which I tried to do by this.off()
- this.$el.off()
/this.undelegateEvents()
is the correct way). But for me, it only fixed the problem that events got called multiple times unnecessarily.
The reason I was plagued by 'zombie views' or unintended behavior was that I wasn't freeing up the memory in the View..
this.remove()
only gets rid of the el
and it's elements/events, but not the View's internal variables. To elaborate - in my View I have an array declared like so this.array: []
and I didn't have it freed in the onClose
function.
All I had to do was empty it in the onClose
function or initially declare the array as this.array: null
so on recurrent View renderings it would at least free the previous array (it still should be freed on the onClose
method though, because the array/object is still going to sit in the memory until browsing away from the page).
It was excruciating to debug, because it's a crossword game (at least my code is hard to read there) and sometimes the words didn't match up, but I didn't know where the problem was coming from.
Lessons learned.