Reputation: 115
I am trying to understand backbone and am currently struggling with zombie views. I have read many stack overflow posts on the matter but I still cannot figure it out.
For the sake of simplicity, I set up two views (without data) that I need to switch. What I did so far was:
//define application object var app = { vent: {}, templates: {}, views: {}, routers: {}, }; //instantiate event aggregator and attach it to app app.vent = _.extend({}, Backbone.Events);
defining two very simple templates (stored into app.templates): the first one has some dummy text and a button (with and id of 'test-begin'), the second one just dummy text
defining two views
app.views.instructions = Backbone.View.extend({ //load underscore template template: _.template(app.templates.instructions), //automatically called upon instantiation initialize: function(options) { //bind relevant fucntions to the view _.bindAll(this, 'render', 'testBegin', 'stillAlive', 'beforeClose'); //listen to app.vent event this.listenTo(app.vent, 'still:alive', this.stillAlive); }, //bind events to DOM elements events: { 'click #test-begin' : 'testBegin', }, //render view render: function() { this.$el.html(this.template()); return this; }, //begin test testBegin: function() { Backbone.history.navigate('begin', {trigger: true}); }, //still alive stillAlive: function() { console.log('I am still alive'); }, //before closing beforeClose: function() { //stop listening to app.vent this.stopListening(app.vent); }, }); //test view app.views.test = Backbone.View.extend({ //load underscore template template: _.template(app.templates.test), //automatically called upon instantiation initialize: function(options) { //trigger still:alive and see if removed view responds to it app.vent.trigger('still:alive'); //bind relevant fucntions to the view _.bindAll(this, 'render'); }, //render view render: function() { this.$el.html(this.template()); return this; }, });
//base router app.routers.baseRouter = Backbone.Router.extend({ //routes routes: { '': "instructions", 'begin': "beginTest" }, //functions (belong to object controller) instructions: function() {baseController.instructions()}, beginTest : function() {baseController.beginTest()}, }); //baseRouter controller var baseController = { instructions: function() { mainApp.viewsManager.rederView(new app.views.instructions()); }, beginTest: function(options) { mainApp.viewsManager.rederView(new app.views.test()); }, };
//define mainApplication object mainApp = {}; //manages views switching mainApp.viewsManager = { //rootEl rootEl: '#test-container', //close current view and show next one rederView : function(view, rootEl) { //if DOM el isn't passed, set it to the default RootEl rootEl = rootEl || this.rootEl; //close current view if (this.currentView) this.currentView.close(); //store reference to next view this.currentView = view; //render next view $(rootEl).html(this.currentView.render().el); }, }; //render first view of app mainApp.viewsManager.rederView(new app.views.instructions()); //initiate router and attach it to app mainApp.baseRouter = new app.routers.baseRouter(); //start Backbone history Backbone.history.start({silent: true });
//add function to Backbone view prototype (available in all views) Backbone.View.prototype.close = function () { //call view beforeClose function if it is defined in the view if (this.beforeClose) this.beforeClose(); //this.el is removed from the DOM & DOM element's events are cleaned up this.remove(); //unbind any model and collection events that the view is bound to this.stopListening(); //check whether view has subviews if (this.hasOwnProperty('_subViews')) { //loop thorugh current view's subviews _(this._subViews).each(function(child){ //invoke subview's close method child.close(); }); } };
So, in order to check for zombie views, the second view triggers and event (still:alive) that the first view listen to and respond to it via a message sent to the console.log (although it really shouldn't). The first view does listen to such a message (in the console log I read 'I am still alive) even when it has been replaced by the second view.
Can you help me? thank you very.
Upvotes: 4
Views: 884
Reputation: 14501
Long post, if you have any questions, please ask
A Zombie View is just a view that is not in the DOM, but listens to and reacts to events -- sometimes this behavior is expected, but not typically.
If the DOM Event handlers for the view are not properly removed, the view and it's in-memory HTML fragments will not be garbage collected. If the Backbone.Event handlers are not unbound properly, you could have all sorts of bad behavior... such as a bunch of "Zombie" view triggering AJAX requests on models. This problem was very common on older versions of Backbone prior to stopListening
and listenTo
especially if you shared models between views.
In your code, you don't have a Zombie View, because you are properly closing your views.
You can see the console.log
because you are initializing the second view (and triggering the event still:alive
) before you close the first view.
To switch views, you are calling:
mainApp.viewsManager.rederView(new app.views.test());
Calling new app.views.test()
initializes the second view which triggers the event that the first listens to.
If you update your code to the following, you won't see the console.log
anymore.
//baseRouter controller
var baseController = {
instructions: function() {
mainApp.viewsManager.rederView(app.views.instructions);
},
beginTest: function(options) {
mainApp.viewsManager.rederView(app.views.test);
},
};
And update rederView
rederView : function(ViewClass, rootEl) {
//if DOM el isn't passed, set it to the default RootEl
rootEl = rootEl || this.rootEl;
//close current view
if (this.currentView) this.currentView.close();
//store reference to next view
this.currentView = new ViewClass();
//render next view
$(rootEl).html(this.currentView.render().el);
},
If you remove this line from your close method, you will have a zombie view and should see the console.log
.
//unbind any model and collection events that the view is bound to
this.stopListening();
In the following code, I am creating 100 views, but only displaying 1 in the DOM. Every view contains the same model and listens to it's change
event. When the view's <button>
element is clicked, it updates the model which causes every view's model change handler to be executed, calling fetch 100 times... 100 AJAX requests!
The view's change handlers are called 100 times, because the view close method does not call this.stopListening()
, so even when the views are removed from the page, they all still listen to the model's events. Once you click the button, the model is changed, and all of the zombie views respond, even though they're not on the page.
var TestView = Backbone.View.extend({
tagName: 'h1',
initialize: function(options) {
this.i = options.i;
this.listenTo(options.model, 'change', function(model) {
model.fetch();
});
},
events: {
'click button': function() {
this.model.set("show_zombies", Date.now());
}
},
render: function() {
this.$el.append("<button>Click To Test for Zombies!</button>");
return this;
},
close: function() {
this.$el.empty(); // empty view html
// this.$el.off(); // // Whoops! Forgot to unbind Event listeners! (this view won't get garbage collected)
// this.stopListening() // Whoops! Forgot to unbind Backbone.Event listeners.
}
});
var model = new (Backbone.Model.extend({
fetch: function() {
document.body.innerHTML += "MODEL.FETCH CALLED<br />"
}
}));
var v;
for (var i = 1; i < 101; i++) {
if (v) v.close();
v = new TestView({
'i': i,
'model': model
}).render();
$('body').html(v.el);
}
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone.js"></script>
Upvotes: 6