Search code examples
backbone.jsmemory-leaks

Backboe memory leak in subviews


I still strugling with memory leak in my app. I wannted to do it without huge changes in code.

var ItemsView = Backbone.View.extend({
    id:'products', // If I change it to el: document.getElementById('products') and without passing views into items object, my views are properly rendered but with memory leak
    events: { },
    initialize: function() {
      _.bindAll(this);
      this.listenTo(this.collection, 'reset',   this.reset);
      this.listenTo(this.collection, 'add',     this.addItem);
      this.listenTo(this.collection, 'change',  this.changeItem);
      this.listenTo(this.collection, 'destroy', this.delItem);
      this.items = [];
    },
    reset: function(){  console.log("reset");
      this.el.innerHTML = null;
      this.render();
    },
    render: function(){
      for(var i=0; i < this.collection.length; i++){
        this.renderItem(this.collection.models[i]);
      }
    },
    renderItem: function( model ){
      var itemView = new ItemView({ model: model });
      itemView.render();
      this.items.push(itemView);
      jBone(this.el).append(itemView.el);
    },
    addItem: function(){ console.log("addItem");
      this.renderItem();
    },
    changeItem: function(){ console.log("changeItem"); },
    delItem: function(){ console.log("delItem"); },
    remove: function() {
      _.invoke(this.items, 'remove');
      this.items = [];
      Backbone.View.prototype.remove.call(this);
    }
});
return ItemsView;

This is my Itemsview it is executed when user hit orderview, there is created ItemView for every model in collection:

var ItemView = Backbone.View.extend({
    tagName: "li",
    className: "productcc",
    initialize: function () {
        _.bindAll(this, 'addItem', 'removeItem', 'updateItem');
        this.listenTo(this.model, 'remove', this.removeItem);
        this.listenTo(this.model, 'change', this.updateItem);
    },
    events: {},
    render: function () {
      var model = this.model.toJSON();
      this.el.innerHTML += '<div class="tabody"><h4 class="tablename">'+model.title+'<h4>'+model.status+'</div>';
      return this;
    },
    addItem: function(){
      this.collection.create({"table_no":"demo"});
    },
    changeItem: function(e){
      e.preventDefault();
      this.model.save({ table_no: 'demo' });
    },
    updateItem: function(newstuff){
      console.log("updateItem");
      console.log(this.el);
    },
    delItem: function(){
      this.model.destroy({ silent: true });
    },
    removeItem: function(model){
      console.log("removeItem");
      console.log(model);
      var self = this;
      self.el.remove();
    }
});
return ItemView;

MY ROUTER:

var AppRouter = Backbone.Router.extend({

    routes: {
               ''        : 'home',
           'home'        : 'home',
        'customer/:customer_id': 'showItems'
    }
});

var initialize = function(options) {

  window.app_router = new AppRouter;
  window.socket     = io.connect('www.example.com');
  this.socketOrdersCollection = new SocketOrdersCollection();
  this.ordersView             = new OrdersView({ collection: this.socketOrdersCollection });
  this.socketOrdersCollection.fetch({ reset: true });

  app_router.on('route:home', function() { });
  app_router.on('route:showItems', function(customer_id) {
    if (this.itemsView) {
      this.itemsView.remove();
    }
    this.socketItemsCollection = new SocketItemsCollection();
    this.socketItemsCollection.fetch({ data: { id: customer_id}, reset: true  });

    this.itemsView = new ItemsView({
      collection: this.socketItemsCollection,
      model: { tableName: customer_id }
    });

  });
  Backbone.history.start();
};

I have to remove also ItemsView after click to another order... Thanks for any opinion.


Solution

  • Ok. Let me take a stab at what you're attempting here.

    var ItemsView = Backbone.View.extend({
        el: document.getElementById('products'),
        events: { },
        initialize: function() {
          // everything you had before
          this.items = [];
        },
        // etc.
        renderItem: function( model ){
          var itemView = new ItemView({ model: model });
          itemView.render();
          this.items.push(itemView);
          jBone(this.el).append(itemView.el);
        },
        // etc.
        // we're overloading the view's remove method, so we clean up our subviews
        remove: function() {
          _.invoke(this.items, 'remove');
          this.items = [];
          Backbone.View.prototype.remove.call(this);
        }
    });
    return ItemsView;
    

    And then in the router:

    var initialize = function(options) {
      // etc.
    
      app_router.on('route:home', function() { });
      app_router.on('route:showItems', function(customer_id) {
        if (this.itemsView) {
          this.itemsView.remove();
        }
    
        // everything else the same
    
      });
      Backbone.history.start();
    };
    

    So now, your ItemsView will clean up any child items it has, and when you change customers, you'll clean up any ItemsView you have open before generating a new one.

    EDIT I see what you're having a problem with now.

    In your route handler, you're going to need to do something along these lines:

    app_router.on('route:showItems', function(customer_id) {
      // everything you already have
    
      jBone(document.getElementById('container')).append(this.itemsView);
    
    });