ChatGPT解决这个技术问题 Extra ChatGPT

Disposing of view and model objects in Backbone.js

Which is the most efficient way to dispose model/view instances when not needed?

Usually, I put all the logic in the controller/router. It is the one that decides, what views should be created, and what models should be supplied to them. Usually, there are a few handler functions, corresponding to different user actions or routes, where I create new view instances every time when a handler gets executed. Of course, that should eliminate whatever I've previously stored in the view instance. However, there are some situations when some views keep DOM event handlers themselves, and they don't get unbinded properly, which results in those instances being kept alive. I wish if there were a proper way to destroy view instances, when for example their el (DOM representation) gets detached, or thrown out of the DOM

if I want to use the same view by changing the model, it is enough to unbind only the model before assigning the new model to view?

M
Mudassir Ali

you're on the right path. you should have an object that controls the lifecycle of your views. i don't like to put this in my view. i like to create a separate object for this.

the thing you need to do, is unbind the events when necessary. to do this, it's a good idea to create a "close" method on all of your views, and use the object that controls the lifecycle of everything to always call the close method.

for example:


  function AppController(){
    this.showView = function (view){
      if (this.currentView){
        this.currentView.close();
      }
      this.currentView = view;
      this.currentView.render();
      $("#someElement").html(this.currentView.el);
    }
  }

at this point, you would set up your code to only have one instance of the AppController, and you would always call appController.showView(...) from your router or any other code that needs to show a view in the #someElement portion of your screen.

(i have another example of a very simple backbone app that uses an "AppView" (a backbone view that runs the main portion of the app), here: http://jsfiddle.net/derickbailey/dHrXv/9/ )

the close method does not exist on views by default, so you need to create one yourself, for each of your views. There are two things that should always be in the close method: this.unbind() and this.remove(). in addition to these, if you are binding your view to any model or collection events, you should unbind them in the close method.

for example:


  MyView = Backbone.View.extend({
    initialize: function(){
      this.model.bind("change", this.modelChanged, this);
    },

    modelChanged: function(){
      // ... do stuff here
    },

    close: function(){
      this.remove();
      this.unbind();
      this.model.unbind("change", this.modelChanged);
    }
  });

this will properly clean up all of the events from the DOM (via this.remove()), all of the events that the view itself may raise (via this.unbind()) and the event that the view bound from the model (via this.model.unbind(...)).


I am having issues with this... stackoverflow.com/questions/19644259/… for some reason when I try creating this function or calling this.remove, etc. after my event trigger it just does not render the page or it seems like it removed the view that is to be rendered.
There has to be this.currentView = null; somewhere right?
S
Shaheen Ghiassy

A simpiler way is to add a custom close method on the Backbone.View object

Backbone.View.prototype.close = function () {
  this.$el.empty();
  this.unbind();
};

Using the above code you can do the following

var myView = new MyView();

myView.close();

easy peasy.


PS: I choose to use this.$el.empty(); instead of this.$el.remove() because .remove() would remove the parent $el from the DOM and I needed that element for later views. If you do in fact want to remove the view's defined el from the DOM, then you can use .remove() instead of .empty()
Excellent example! I have struggled myself with removing the $el from the DOM and losing my mind debugging why subsequent views could not be established. I didn't even know $el.empty() existed and created some goofy workarounds.
d
djabraham

I always nuke the views and sometimes reuse the models. Making sure the views are deallocated can be a pain, if you keep the models around. The models may keep a reference to the view if they are not unbound properly.

As of Backbone ~0.9.9, binding models with view.listenTo() rather than model.on() allows for easier cleanup through inversion of control (views control bindings as opposed to models). If view.listenTo() is used to bind, then a call to view.stopListening() or view.remove() will remove all bindings. Similar to calling model.off(null, null, this).

I like to cleanup views by extending the view with a close function that calls sub-views semi-automatically. The subviews must be referenced by properties of the parent or they must be added to an array within the parent called childViews[].

Here is the close function that I use..

// fired by the router, signals the destruct event within top view and 
// recursively collapses all the sub-views that are stored as properties
Backbone.View.prototype.close = function () {

    // calls views closing event handler first, if implemented (optional)
    if (this.closing) {
        this.closing();  // this for custom cleanup purposes
    }

    // first loop through childViews[] if defined, in collection views
    //  populate an array property i.e. this.childViews[] = new ControlViews()
    if (this.childViews) {
        _.each(this.childViews, function (child) {
            child.close();
        });
    }

    // close all child views that are referenced by property, in model views
    //  add a property for reference i.e. this.toolbar = new ToolbarView();
    for (var prop in this) {
        if (this[prop] instanceof Backbone.View) {
            this[prop].close();
        }
    }

    this.unbind();
    this.remove();

    // available in Backbone 0.9.9 + when using view.listenTo, 
    //  removes model and collection bindings
    // this.stopListening(); // its automatically called by remove()

    // remove any model bindings to this view 
    //  (pre Backbone 0.9.9 or if using model.on to bind events)
    // if (this.model) {
    //  this.model.off(null, null, this);
    // }

    // remove and collection bindings to this view 
    //  (pre Backbone 0.9.9 or if using collection.on to bind events)
    // if (this.collection) {
    //  this.collection.off(null, null, this);
    // }
}

Then a view is declared as follows..

views.TeamView = Backbone.View.extend({

    initialize: function () {
        // instantiate this array to ensure sub-view destruction on close()
        this.childViews = [];  

        this.listenTo(this.collection, "add", this.add);
        this.listenTo(this.collection, "reset", this.reset);

        // storing sub-view as a property will ensure destruction on close()
        this.editView = new views.EditView({ model: this.model.edits });
        $('#edit', this.el).html(this.editView.render().el);
    },

    add: function (member) {
        var memberView = new views.MemberView({ model: member });
        this.childViews.push(memberView);    // add child to array

        var item = memberView.render().el;
        this.$el.append(item);
    },

    reset: function () {
        // manually purge child views upon reset
        _.each(this.childViews, function (child) {
            child.close();
        });

        this.childViews = [];
    },

    // render is called externally and should handle case where collection
    // was already populated, as is the case if it is recycled
    render: function () {
        this.$el.empty();

        _.each(this.collection.models, function (member) {
            this.add(member);
        }, this);
        return this;
    }

    // fired by a prototype extension
    closing: function () {
        // handle other unbinding needs, here
    }
});

Nice, this is way cool! I love the prop checking for instanceof Backbone.View, well done.