Pierpaolo Calanna
Pierpaolo Calanna

Reputation: 115

Again on Backbone Zombie Views

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:

  1. creating an object

    //define application object
    var app = {
      vent: {},
      templates: {},
      views: {},
      routers: {},
    };

    //instantiate event aggregator and attach it to app
    app.vent = _.extend({}, Backbone.Events);

  1. 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

  2. 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;
        },
    });

  1. defining a router

    //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());
        },
    };

  1. defining mainApp (with a view-switcher)

    //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

});
  1. adding a close function to view via Backbone prototype


    //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

Answers (1)

Cory Danielson
Cory Danielson

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(); 


Zombie View Example

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

Related Questions