Backbone.js: Die Ansicht neu füllen oder neu erstellen?

83

In meiner Webanwendung habe ich links eine Benutzerliste in einer Tabelle und rechts einen Benutzerdetailbereich. Wenn der Administrator auf einen Benutzer in der Tabelle klickt, sollten dessen Details rechts angezeigt werden.

Ich habe links eine UserListView und UserRowView und rechts eine UserDetailView. Die Dinge funktionieren, aber ich habe ein komisches Verhalten. Wenn ich links auf einige Benutzer klicke und dann auf einen von ihnen klicke, werden nacheinander Javascript-Bestätigungsfelder für alle angezeigten Benutzer angezeigt.

Es sieht so aus, als ob Ereignisbindungen aller zuvor angezeigten Ansichten nicht entfernt wurden, was normal zu sein scheint. Ich sollte nicht jedes Mal eine neue UserDetailView in UserRowView erstellen. Sollte ich eine Ansicht beibehalten und das Referenzmodell ändern? Sollte ich die aktuelle Ansicht verfolgen und entfernen, bevor ich eine neue erstelle? Ich bin irgendwie verloren und jede Idee wird willkommen sein. Danke !

Hier ist der Code der linken Ansicht (Zeilenanzeige, Klickereignis, Erstellung der rechten Ansicht)

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

Und der Code für die rechte Ansicht (Schaltfläche Löschen)

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;
    }
})
Solendil
quelle

Antworten:

28

Ich habe kürzlich darüber gebloggt und einige Dinge gezeigt, die ich in meinen Apps mache, um mit diesen Szenarien umzugehen:

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

Derick Bailey
quelle
1
Warum nicht einfach delete viewim Router?
Trantor Liu
Ich habe Ihre Antwort positiv bewertet, aber es wäre wirklich von Vorteil, wenn die relevanten Teile des Blogposts in der Antwort selbst enthalten wären, da dies das Ziel hier ist.
Emile Bergeron
136

Ich zerstöre und erstelle immer Ansichten, da es schwierig wird, nicht verwendete Live-Ansichten im Speicher zu behalten, damit meine wiederverwendbaren Ansichten immer größer werden, damit ich sie wiederverwenden kann.

Hier ist eine vereinfachte Version einer Technik, mit der ich meine Ansichten bereinige, um Speicherlecks zu vermeiden.

Ich erstelle zuerst eine BaseView, von der alle meine Ansichten erben. Die Grundidee ist, dass meine Ansicht einen Verweis auf alle Ereignisse enthält, die sie abonniert hat, sodass alle diese Bindungen automatisch ungebunden sind, wenn es Zeit ist, die Ansicht zu entsorgen. Hier ist eine Beispielimplementierung meiner 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;

Wann immer eine Ansicht an ein Ereignis in einem Modell oder einer Sammlung gebunden werden muss, verwende ich die bindTo-Methode. Beispielsweise:

var SampleView = BaseView.extend({

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

Immer wenn ich eine Ansicht entferne, rufe ich einfach die dispose-Methode auf, die alles automatisch bereinigt:

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

Ich habe diese Technik mit den Leuten geteilt, die das E-Book "Backbone.js on Rails" schreiben, und ich glaube, dies ist die Technik, die sie für das Buch übernommen haben.

Update: 24.03.2014

Ab Backone 0.9.9 wurden ListenTo und StopListening zu Ereignissen hinzugefügt, wobei dieselben oben gezeigten Techniken bindTo und unbindFromAll verwendet wurden. Außerdem ruft View.remove stopListening automatisch auf, sodass das Binden und Aufheben der Bindung jetzt so einfach ist:

var SampleView = BaseView.extend({

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

var sampleView = new SampleView({model: some_model});
sampleView.remove();
Johnny Oshika
quelle
Haben Sie Vorschläge zum Entsorgen verschachtelter Ansichten? Im Moment mache ich es ähnlich wie bei bindTo: gist.github.com/1288947, aber ich denke, es ist möglich, etwas besseres zu machen.
Dmitry Polushkin
Dmitry, ich mache etwas Ähnliches wie Sie, um verschachtelte Ansichten zu entsorgen. Ich habe noch keine bessere Lösung gesehen, aber ich würde mich auch interessieren, ob es eine gibt. Hier ist eine weitere Diskussion, die dies ebenfalls berührt: groups.google.com/forum/#!topic/backbonejs/3ZFm-lteN-A . Ich habe festgestellt, dass Sie in Ihrer Lösung das Szenario nicht berücksichtigen, in dem eine verschachtelte Ansicht direkt angeordnet wird. In einem solchen Szenario enthält die übergeordnete Ansicht weiterhin einen Verweis auf die verschachtelte Ansicht, obwohl die verschachtelte Ansicht angeordnet ist. Ich weiß nicht, ob Sie das erklären müssen.
Johnny Oshika
Was ist, wenn ich Funktionen habe, die dieselbe Ansicht öffnen und schließen? Ich habe einen Vorwärts- und Rückwärtsknopf. Wenn ich dispose aufrufe, wird das Element aus dem DOM entfernt. Soll ich die Ansicht die ganze Zeit im Gedächtnis behalten?
dagda1
1
Hallo Fisherwebdev. Sie können diese Technik auch mit Backbone.View.extend verwenden, müssen diese thisbindings jedoch in der BaseView.initialize-Methode initialisieren. Das Problem dabei ist, dass wenn Ihre geerbte Ansicht ihre eigene Initialisierungsmethode implementiert, sie die Initialisierungsmethode von BaseView explizit aufrufen muss. Ich habe dieses Problem hier ausführlicher erklärt: stackoverflow.com/a/7736030/188740
Johnny Oshika
2
Hallo SunnyRed, ich habe meine Antwort aktualisiert, um meinen Grund für die Zerstörung von Ansichten besser widerzuspiegeln. Mit Backbone sehe ich keinen Grund, eine Seite nach dem Start einer App neu zu laden, daher ist meine App für einzelne Seiten ziemlich groß geworden. Während Benutzer mit meiner App interagieren, rendere ich ständig verschiedene Abschnitte der Seite neu (z. B. Wechseln von Detail- zu Bearbeitungsansicht), sodass ich es viel einfacher finde, immer neue Ansichten zu erstellen, unabhängig davon, ob dieser Abschnitt zuvor gerendert wurde oder nicht nicht. Modelle hingegen stellen Geschäftsobjekte dar, sodass ich sie nur ändern würde, wenn sich das Objekt wirklich geändert hätte.
Johnny Oshika
8

Dies ist eine häufige Erkrankung. Wenn Sie jedes Mal eine neue Ansicht erstellen, werden alle alten Ansichten weiterhin an alle Ereignisse gebunden. Sie können in Ihrer Ansicht eine Funktion mit dem Namen erstellen detatch:

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

Stellen Sie dann sicher, dass Sie anrufen, bevor Sie die neue Ansicht erstellen detatch die alte Ansicht .

Wie Sie bereits erwähnt haben, können Sie natürlich immer eine "Detail" -Ansicht erstellen und diese niemals ändern. Sie können an das Ereignis "Ändern" im Modell (aus der Ansicht) binden, um sich selbst neu zu rendern. Fügen Sie dies Ihrem Initialisierer hinzu:

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

Dadurch wird das Detailfenster jedes Mal neu gerendert, wenn eine Änderung am Modell vorgenommen wird. Sie können eine feinere Granularität erzielen, indem Sie nach einer einzelnen Eigenschaft suchen: "change: propName".

Dazu ist natürlich ein allgemeines Modell erforderlich, auf das sich die Elementansicht bezieht, sowie die Listenansicht auf höherer Ebene und die Detailansicht.

Hoffe das hilft!

Brian Genisio
quelle
1
Hmmm, ich habe etwas in der von Ihnen vorgeschlagenen Richtung getan, aber ich habe immer noch Probleme: Zum Beispiel ist das this.model.unbind()für mich falsch, weil es alle Ereignisse aus diesem Modell aufhebt, einschließlich Ereignisse in Bezug auf andere Ansichten desselben Benutzers. Um die detachFunktion aufzurufen , muss ich außerdem einen statischen Verweis auf die Ansicht behalten, und das gefällt mir nicht. Ich vermute, es gibt noch etwas, das ich nicht verstanden habe ...
Solendil
6

Um mehrfach bindende Ereignisse zu beheben,

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

Durch die Verwendung der obigen Zeile vor dem Instanziieren der neuen Ansichten von der Route wurde das Problem mit Zombie-Ansichten behoben.

Ashan
quelle
Hier gibt es viele sehr gute, detaillierte Antworten. Ich beabsichtige auf jeden Fall, einige der ViewManger-Vorschläge zu prüfen. Dieser war jedoch kinderleicht und funktioniert perfekt für mich, da meine Ansichten alle Panels mit close () -Methoden sind, bei denen ich die Ereignisse einfach aufheben kann. Danke Ashan
netpoetica
2
Ich kann nicht scheinen, neu zu rendern, nachdem ich binde: \
CodeGuru
@FlyingAtom: Auch ich kann Ansichten nach dem Aufheben der Bindung nicht erneut rendern. Haben Sie einen Weg gefunden, das zu tun?
Raeesaa
view. $ el.removeData (). unbind ();
Alexander Mills
2

Ich denke, die meisten Leute, die mit Backbone beginnen, erstellen die Ansicht wie in Ihrem Code:

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

Dieser Code erstellt eine Zombie-Ansicht, da wir möglicherweise ständig eine neue Ansicht erstellen, ohne die vorhandene Ansicht zu bereinigen. Es ist jedoch nicht bequem, view.dispose () für alle Backbone-Ansichten in Ihrer App aufzurufen (insbesondere, wenn wir Ansichten in for-Schleife erstellen).

Ich denke, der beste Zeitpunkt, um Bereinigungscode einzufügen, ist vor dem Erstellen einer neuen Ansicht. Meine Lösung besteht darin, einen Helfer für diese Bereinigung zu erstellen:

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];
}

Durch die Verwendung von VM zum Erstellen Ihrer Ansicht können Sie vorhandene Ansichten bereinigen, ohne view.dispose () aufrufen zu müssen. Sie können eine kleine Änderung an Ihrem Code vornehmen

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

zu

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

Es liegt also an Ihnen, ob Sie die Ansicht wiederverwenden möchten, anstatt sie ständig zu erstellen. Solange die Ansicht sauber ist, müssen Sie sich keine Sorgen machen. Ändern Sie einfach createView in reuseView:

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

Detaillierte Informationen zu Code und Zuordnung finden Sie unter https://github.com/thomasdao/Backbone-View-Manager

thomasdao
quelle
Ich habe in letzter Zeit ausgiebig mit Backbone gearbeitet und dies scheint das umfassendste Mittel zu sein, um Zombie-Ansichten beim Erstellen oder Wiederverwenden von Ansichten zu verarbeiten. Normalerweise folge ich den Beispielen von Derick Bailey, aber in diesem Fall scheint dies flexibler zu sein. Meine Frage ist, warum nicht mehr Menschen diese Technik anwenden.
MFD3000
Vielleicht, weil er Experte für Backbone ist :). Ich denke, diese Technik ist ziemlich einfach und ziemlich sicher zu verwenden, ich habe sie benutzt und habe bisher keine Probleme :)
Thomasdao
0

Eine Alternative besteht darin, zu binden, anstatt eine Reihe neuer Ansichten zu erstellen und diese Ansichten dann zu lösen. Sie würden dies erreichen, indem Sie Folgendes tun:

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"); 
    }
}); 

Sie würden das Modell von myView auf myViewModel setzen, das auf ein Benutzermodell gesetzt würde. Wenn Sie myViewModel auf einen anderen Benutzer festlegen (dh dessen Attribute ändern), kann auf diese Weise eine Renderfunktion in der Ansicht mit den neuen Attributen ausgelöst werden.

Ein Problem ist, dass dadurch die Verbindung zum Originalmodell unterbrochen wird. Sie können dies umgehen, indem Sie entweder ein Sammlungsobjekt verwenden oder das Benutzermodell als Attribut des Ansichtsmodells festlegen. Dann wäre dies in der Ansicht als myview.model.get ("Modell") zugänglich.

Bento
quelle
1
Eine Verschmutzung des globalen Geltungsbereichs ist niemals eine gute Idee. Warum sollten Sie BB.Models und BB.Views im Fensternamensraum instanziieren?
Vernon
0

Verwenden Sie diese Methode, um die untergeordneten Ansichten und aktuellen Ansichten aus dem Speicher zu löschen.

//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);
       }
  });
Robins Gupta
quelle