ChatGPT解决这个技术问题 Extra ChatGPT

Backbone.js : repopulate or recreate the view?

In my web application, I have a user list in a table on the left, and a user detail pane on the right. When the admin clicks a user in the table, its details should be displayed on the right.

I have a UserListView and UserRowView on the left, and a UserDetailView on the right. Things kind of work, but I have a weird behavior. If I click some users on the left, then click delete on one of them, I get successive javascript confirm boxes for all users that have been displayed.

It looks like event bindings of all previously displayed views have not been removed, which seems to be normal. I should not do a new UserDetailView every time on UserRowView? Should I maintain a view and change its reference model? Should I keep track of the current view and remove it before creating a new one? I'm kind of lost and any idea will be welcome. Thank you !

Here is the code of the left view (row display, click event, right view creation)

window.UserRowView = Backbone.View.extend({
    tagName : "tr",
    events : {
        "click" : "click",
    },
    render : function() {
        $(this.el).html(ich.bbViewUserTr(this.model.toJSON()));
        return this;
    },
    click : function() {
        var view = new UserDetailView({model:this.model})
        view.render()
    }
})

And the code for right view (delete button)

window.UserDetailView = Backbone.View.extend({
    el : $("#bbBoxUserDetail"),
    events : {
        "click .delete" : "deleteUser"
    },
    initialize : function() {
        this.model.bind('destroy', function(){this.el.hide()}, this);
    },
    render : function() {
        this.el.html(ich.bbViewUserDetail(this.model.toJSON()));
        this.el.show();
    },
    deleteUser : function() {
        if (confirm("Really delete user " + this.model.get("login") + "?")) 
            this.model.destroy();
        return false;
    }
})

J
Johnny Oshika

I always destroy and create views because as my single page app gets bigger and bigger, keeping unused live views in memory just so that I can re-use them would become difficult to maintain.

Here's a simplified version of a technique that I use to clean-up my Views to avoid memory leaks.

I first create a BaseView that all of my views inherit from. The basic idea is that my View will keep a reference to all of the events to which it's subscribed to, so that when it's time to dispose the View, all of those bindings will automatically be unbound. Here's an example implementation of my BaseView:

var BaseView = function (options) {

    this.bindings = [];
    Backbone.View.apply(this, [options]);
};

_.extend(BaseView.prototype, Backbone.View.prototype, {

    bindTo: function (model, ev, callback) {

        model.bind(ev, callback, this);
        this.bindings.push({ model: model, ev: ev, callback: callback });
    },

    unbindFromAll: function () {
        _.each(this.bindings, function (binding) {
            binding.model.unbind(binding.ev, binding.callback);
        });
        this.bindings = [];
    },

    dispose: function () {
        this.unbindFromAll(); // Will unbind all events this view has bound to
        this.unbind();        // This will unbind all listeners to events from 
                              // this view. This is probably not necessary 
                              // because this view will be garbage collected.
        this.remove(); // Uses the default Backbone.View.remove() method which
                       // removes this.el from the DOM and removes DOM events.
    }

});

BaseView.extend = Backbone.View.extend;

Whenever a View needs to bind to an event on a model or collection, I would use the bindTo method. For example:

var SampleView = BaseView.extend({

    initialize: function(){
        this.bindTo(this.model, 'change', this.render);
        this.bindTo(this.collection, 'reset', this.doSomething);
    }
});

Whenever I remove a view, I just call the dispose method which will clean everything up automatically:

var sampleView = new SampleView({model: some_model, collection: some_collection});
sampleView.dispose();

I shared this technique with the folks who are writing the "Backbone.js on Rails" ebook and I believe this is the technique that they've adopted for the book.

Update: 2014-03-24

As of Backone 0.9.9, listenTo and stopListening were added to Events using the same bindTo and unbindFromAll techniques shown above. Also, View.remove calls stopListening automatically, so binding and unbinding is as easy as this now:

var SampleView = BaseView.extend({

    initialize: function(){
        this.listenTo(this.model, 'change', this.render);
    }
});

var sampleView = new SampleView({model: some_model});
sampleView.remove();

Do you have any suggestions how to dispose nested views? Right now I'm doing similar to the bindTo: gist.github.com/1288947 but I guess it's possible to do something more better.
Dmitry, I do something similar to what you're doing to dispose nested views. I haven't yet seen a better solution, but I'd also be interested to know if there is one. Here's another discussion that touches on this as well: groups.google.com/forum/#!topic/backbonejs/3ZFm-lteN-A. I noticed that in your solution, you're not taking into account the scenario where a nested view gets disposed directly. In such a scenario, the parent view will still hold a reference to the nested view even though the nested view is disposed. I don't know if you need to account for this.
What if I have functionality that opens and closes the same view. I have a forwards and backwards buttons. If I call dispose it will remove the element from the DOM. Should I keep the view in memory the whole time?
Hi fisherwebdev. You can also use this technique with Backbone.View.extend, but you will need to initialize this.bindings in the BaseView.initialize method. The problem with this is that if your inherited view implements its own initialize method, then it will need to explicitly call BaseView's initialize method. I explained this problem in more detail here: stackoverflow.com/a/7736030/188740
Hi SunnyRed, I updated my answer to better reflect my reason for destroying views. With Backbone, I see no reason to ever reload a page after an app starts, so my single page app has gotten quite large. As users interact with my app, I'm constantly re-rendering different sections of the page (e.g. switching from detail to edit view), so I find it much easier to always create new views, regardless of whether that section was previously rendered or not. Models on the other hand represent business objects, so I would only modify them if the object really changed.
D
Derick Bailey

I blogged about this recently, and showed several things that I do in my apps to handle these scenarios:

http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/


Why not just delete view in the router?
I upvoted your answer, but it would really benefit from having the relevant parts of the blog post inside the answer itself as it's the goal here.
B
Brian Genisio

This is a common condition. If you create a new view every time, all old views will still be bound to all of the events. One thing you can do is create a function on your view called detatch:

detatch: function() {
   $(this.el).unbind();
   this.model.unbind();

Then, before you create the new view, make sure to call detatch on the old view.

Of course, as you mentioned, you can always create one "detail" view and never change it. You can bind to the "change" event on the model (from the view) to re-render yourself. Add this to your initializer:

this.model.bind('change', this.render)

Doing that will cause the details pane to re-render EVERY time a change is made to the model. You can get finer granularity by watching for a single property: "change:propName".

Of course, doing this requires a common model that the item View has reference to as well as the higher level list view and the details view.

Hope this helps!


Hmmm, I did something along the lines you suggested, but I still have problems : for example, the this.model.unbind() is wrong for me because it unbinds all events from this model, including events regarding other views of the same user. Moreover, in order to call the detach function, I need to keep a static reference to the view, and I quite don't like it. I suspect there's still something I haven't undertand...
A
Ashan

To fix events binding multiple times,

$("#my_app_container").unbind()
//Instantiate your views here

Using the above line before instantiating the new Views from route, solved the issue I had with zombie views.


There are lots of very good, detailed answers here. I definitely intend to look into some of the ViewManger suggestions. However, this one was dead simple and it works perfectly for me because my Views are all Panels with close() methods, where I can just unbind the events. Thanks Ashan
I cant seem to re-render after i unbind :\
@FlyingAtom: Even I am not being able to re-render views after unbinding. Did you find any way to do that?
view.$el.removeData().unbind();
t
thomasdao

I think most people start with Backbone will create the view as in your code:

var view = new UserDetailView({model:this.model});

This code creates zombie view, because we might constantly create new view without cleanup existing view. However it's not convenient to call view.dispose() for all Backbone Views in your app (especially if we create views in for loop)

I think the best timing to put cleanup code is before creating new view. My solution is to create a helper to do this cleanup:

window.VM = window.VM || {};
VM.views = VM.views || {};
VM.createView = function(name, callback) {
    if (typeof VM.views[name] !== 'undefined') {
        // Cleanup view
        // Remove all of the view's delegated events
        VM.views[name].undelegateEvents();
        // Remove view from the DOM
        VM.views[name].remove();
        // Removes all callbacks on view
        VM.views[name].off();

        if (typeof VM.views[name].close === 'function') {
            VM.views[name].close();
        }
    }
    VM.views[name] = callback();
    return VM.views[name];
}

VM.reuseView = function(name, callback) {
    if (typeof VM.views[name] !== 'undefined') {
        return VM.views[name];
    }

    VM.views[name] = callback();
    return VM.views[name];
}

Using VM to create your view will help cleanup any existing view without having to call view.dispose(). You can do a small modification to your code from

var view = new UserDetailView({model:this.model});

to

var view = VM.createView("unique_view_name", function() {
                return new UserDetailView({model:this.model});
           });

So it is up to you if you want to reuse view instead of constantly creating it, as long as the view is clean, you don't need to worry. Just change createView to reuseView:

var view = VM.reuseView("unique_view_name", function() {
                return new UserDetailView({model:this.model});
           });

Detailed code and attribution is posted at https://github.com/thomasdao/Backbone-View-Manager


Ive been working with backbone extensively lately and this seems to be the most fleshed out means of handling zombie views when building or reusing views. I normally follow Derick Bailey's examples, but in this case, this seems more flexible. My question is, why arent more people using this technique?
maybe because he is expert in Backbone :). I think this technique is quite simple and quite safe to use, I've been using it and don't have problem so far :)
b
bento

One alternative is to bind, as opposed to creating a series of new views and then unbinding those views. You'd accomplish this doing something like:

window.User = Backbone.Model.extend({
});

window.MyViewModel = Backbone.Model.extend({
});

window.myView = Backbone.View.extend({
    initialize: function(){
        this.model.on('change', this.alert, this); 
    },
    alert: function(){
        alert("changed"); 
    }
}); 

You'd set the model of myView to myViewModel, which would be set to a User model. This way, if you set myViewModel to another user (i.e., changing its attributes) then it could trigger a render function in the view with the new attributes.

One problem is that this breaks the link to the original model. You could get around this by either using a collection object, or by setting the user model as an attribute of the viewmodel. Then, this would be accessible in the view as myview.model.get("model").


Polluting global scope is never a good idea. Why would you instantiate BB.Models and BB.Views on the window namespace?
R
Robins Gupta

Use this method for clearing the child views and current views from memory.

//FIRST EXTEND THE BACKBONE VIEW....
//Extending the backbone view...
Backbone.View.prototype.destroy_view = function()
{ 
   //for doing something before closing.....
   if (this.beforeClose) {
       this.beforeClose();
   }
   //For destroying the related child views...
   if (this.destroyChild)
   {
       this.destroyChild();
   }
   this.undelegateEvents();
   $(this.el).removeData().unbind(); 
  //Remove view from DOM
  this.remove();  
  Backbone.View.prototype.remove.call(this);
 }



//Function for destroying the child views...
Backbone.View.prototype.destroyChild  = function(){
   console.info("Closing the child views...");
   //Remember to push the child views of a parent view using this.childViews
   if(this.childViews){
      var len = this.childViews.length;
      for(var i=0; i<len; i++){
         this.childViews[i].destroy_view();
      }
   }//End of if statement
} //End of destroyChild function


//Now extending the Router ..
var Test_Routers = Backbone.Router.extend({

   //Always call this function before calling a route call function...
   closePreviousViews: function() {
       console.log("Closing the pervious in memory views...");
       if (this.currentView)
           this.currentView.destroy_view();
   },

   routes:{
       "test"    :  "testRoute"
   },

   testRoute: function(){
       //Always call this method before calling the route..
       this.closePreviousViews();
       .....
   }


   //Now calling the views...
   $(document).ready(function(e) {
      var Router = new Test_Routers();
      Backbone.history.start({root: "/"}); 
   });


  //Now showing how to push child views in parent views and setting of current views...
  var Test_View = Backbone.View.extend({
       initialize:function(){
          //Now setting the current view..
          Router.currentView = this;
         //If your views contains child views then first initialize...
         this.childViews = [];
         //Now push any child views you create in this parent view. 
         //It will automatically get deleted
         //this.childViews.push(childView);
       }
  });