Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Backbone zombie views & good practice

I'm fairly new to backbone and I try to understand the ins and outs of zombie views.

A zombie is, according to this article:

When we bind objects together through events but we don’t bother unbinding them. As long as these objects are bound together, and there is a reference in our app code to at least one of them, they won’t be cleaned up or garbage collected. The resulting memory leaks are like the zombies of the movies – hiding in dark corners, waiting to jump out and eat us for lunch.

The article mentionned above suggests to create an object that manages the transitions between views and then to implement a close function to remove and unbind the view.

That being said, depending on the situation, where to call that close function from?

I add a property in the initialize block of my parent view to keep a trace of the child view. That way I'm able to call .remove() on it before I replace it by a new one. Is it good practice or is there a better way?

I also don't understand why defining el and then rendering with

this.$el.html(this.template(this.model.attributes));

doesn't allow me to unbind the view while it works as expected by doing

$('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));

As for the exemple, I just created a simple app that displays a list of sportsmen's names and that shows more details when clicking on a name.

Here's the code and a working fiddle:

html

<script id="nameListTemplate" type="text/template">
    <%= first %> <%= last %>
</script>
<script id="sportsManDetailsTemplate" type="text/template">
    <ul>
        <li><%= first %></li>
        <li><%= last %></li>
        <li><%= age %></li>
        <li><%= sport %></li>
        <li><%= category %></li>
    </ul>
    <button class="test">Test</button>
</script>
<div id="sportsMenName"></div>
<div id="sportsManDetails"></div>

JS

model and collection

var app = app || {};

app.SportsManModel = Backbone.Model.extend({});

app.SportsMenCollection = Backbone.Collection.extend({
    model: app.SportsManModel
});

NameView

app.NameView = Backbone.View.extend({
    tagName: 'li',
    className: 'sportsMan',
    template: _.template($('#nameListTemplate').html()),

    initialize: function(){
        this.sportsManDetailsView;  
    },

    events: {
        'click': 'showSportsManDetails'
    },

    showSportsManDetails: function(e){
        if (typeof this.sportsManDetailsView !== 'undefined'){
            this.sportsManDetailsView.remove();
        }
        this.sportsManDetailsView = new app.SportsManDetailsView({
            model: this.model
        })  
    },

    render: function(){
        this.$el.append(this.template(this.model.attributes));
        return this;
    }
});

NameListView

app.NameListView = Backbone.View.extend({
    el: '#sportsMenName',

    initialize: function(sportsMen){
        this.collection = new app.SportsMenCollection(sportsMen);
        this.render();
    },

    render: function(){
        this.collection.each(function(sportsMen){
            this.renderContact(sportsMen);
        }, this);
    },

    renderContact: function(sportsMen){
        var nameView = new app.NameView({
            model: sportsMen   
        });
        this.$el.append(nameView.render().el);
    }
});

SportsManDetailsView

app.SportsManDetailsView = Backbone.View.extend({
    // doesn't work if I use el in conjunction with 
    // this.$el.html(this.template(this.model.attributes));
    // el: '#sportsManDetails',
    template: _.template($('#sportsManDetailsTemplate').html()),

    initialize: function(){
        this.render();
    },

    events: {
        'click .test': 'test'
    },

    test: function(){
        alert('test');  
    },

    render: function(){                      
        // that does not work
        //this.$el.html(this.template(this.model.attributes));

        // is this good practice?
        $('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));
    }
});

app.js

var sportsMen = [
    {first: 'Quentin', last: 'Tarant', age: '34', sport: 'bike', category: '- 90kg'},
    {first: 'Aymeric', last: 'McArthur', age: '54', sport: 'jetski', category: '200HP'},
    {first: 'Peter', last: 'TheFat', age: '45', sport: 'curling', category: 'dunno'},
    {first: 'Charles', last: 'Martel', age: '21', sport: 'Moto', category: 'MX 250cc'},
];

$(function(){
    new app.NameListView(sportsMen);
});
like image 486
Buzut Avatar asked Sep 18 '14 14:09

Buzut


1 Answers

Just as you're discovering, Backbone considers itself more of a library than a framework - it leaves a lot of questions and design patterns left to the developer.

The term "zombie view" is used to designate views that are still bound to something (and thus alive) when you think they're dead. Usually there's a leftover reference to the view from a model.on call or similar. Basically a specific form of memory leak.

To manage the life cycle of the view, you can use a parent view, but it is a common practice to do this from a router. The router is used to remove old views and instantiate new ones upon a route event. Here's a snippet of how I often accomplish this:

render: function(){
    this.mainView && this.mainView.remove();                    // if there is already a view, remove it
    this.mainView = new SomeOtherKindOfViewDeterminedBySomeEvent(); // instantiate the new view
    this.mainView.render();
    this.mainView.$el.appendTo( '#main-content' );              // append it
}

Some things to note:

  1. Without explicitly calling remove on a view, your app will be vulnerable to memory leaks. This is because the View's events and properties still exist in the background. For example, if you remove the first line of the example above, I will lose my reference to the former this.mainView, but it's events are still using memory. This will have an effect on your app over time.
  2. Note that I'm using appendTo in the last line. When calling remove on a View, it's entire element is removed, as well as it's events. If I had simply done this:

    this.mainView = new SomeOtherKindOfViewDeterminedBySomeEvent({ el: '#main-content' })

    Then after I call remove on this.mainView, #main-content will have been removed from the DOM, so I can no longer use that selector. By appending it, I keep that #main-content around as a placeholder, so I can continue to append views to it. This is what you are seeing when trying to unbind SportsManDetailsView then render it again.

As for your questions, this:

$('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));

Is not good practice. This first is that you've used the global jQuery object, which defeats Backbone's approach of encapsulated views. Second, the events are still active in the DOM from former views, leading to memory leaks. You can see this when you click the Test button - the handler function will fire for every time you instantiated a SportsManDetailsView (the second time around, the alert message will be shown twice, then three times, etc.)

You should rely on a parent view or router to handle such interaction. That, or keep your SportsManDetailsView bound to the #sportsManDetails element, and never remove it. Then when the click event occurs in your NameView, have its model trigger trigger an event. Then your SportsManDetailsView can listen for the event in the corresponding collection and re-render itself accordingly. Embrace Backbone's events! JavaScript is an event-driven language, and never forget that you have those in your artillery.

I have updated your JSFiddle to demonstrate some of what I've talked about.

like image 117
ncksllvn Avatar answered Oct 23 '22 09:10

ncksllvn