Backbone.js: ponownie zapełnij lub odtwórz widok?

83

W mojej aplikacji internetowej mam listę użytkowników w tabeli po lewej stronie i okienko szczegółów użytkownika po prawej stronie. Kiedy administrator kliknie użytkownika w tabeli, jego szczegóły powinny zostać wyświetlone po prawej stronie.

Mam UserListView i UserRowView po lewej stronie i UserDetailView po prawej stronie. Niby wszystko działa, ale mam dziwne zachowanie. Jeśli kliknę niektórych użytkowników po lewej stronie, a następnie kliknę usuń na jednym z nich, otrzymam kolejne pola potwierdzenia javascript dla wszystkich wyświetlonych użytkowników.

Wygląda na to, że powiązania zdarzeń wszystkich wcześniej wyświetlanych widoków nie zostały usunięte, co wydaje się normalne. Nie powinienem za każdym razem tworzyć nowego UserDetailView na UserRowView? Czy powinienem zachować widok i zmienić jego model referencyjny? Czy powinienem śledzić bieżący widok i usunąć go przed utworzeniem nowego? Jestem trochę zagubiony i każdy pomysł będzie mile widziany. Dziękuję Ci !

Oto kod lewego widoku (wyświetlanie wierszy, kliknięcie, tworzenie prawego widoku)

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

I kod do prawego widoku (przycisk usuwania)

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
źródło

Odpowiedzi:

28

Niedawno pisałem o tym na blogu i pokazałem kilka rzeczy, które robię w moich aplikacjach, aby poradzić sobie z tymi scenariuszami:

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

Derick Bailey
źródło
1
Dlaczego nie tylko delete vieww routerze?
Trantor Liu
Głosowałem za twoją odpowiedzią, ale byłoby naprawdę korzystne, gdyby odpowiednie części wpisu na blogu znalazły się w samej odpowiedzi, ponieważ jest to tutaj cel.
Emile Bergeron
136

Zawsze niszczę i tworzę widoki, ponieważ w miarę jak moja pojedyncza aplikacja strony staje się coraz większa, utrzymanie w pamięci nieużywanych widoków na żywo tylko po to, aby móc ich ponownie użyć, byłoby trudne do utrzymania.

Oto uproszczona wersja techniki, której używam do czyszczenia widoków, aby uniknąć wycieków pamięci.

Najpierw tworzę BaseView, z którego dziedziczą wszystkie moje widoki. Podstawową ideą jest to, że mój widok zachowa odniesienie do wszystkich zdarzeń, do których jest zasubskrybowany, więc kiedy nadejdzie czas na usunięcie widoku, wszystkie te powiązania zostaną automatycznie niezwiązane. Oto przykładowa implementacja mojego 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;

Ilekroć widok musi powiązać się ze zdarzeniem w modelu lub kolekcji, użyłbym metody bindTo. Na przykład:

var SampleView = BaseView.extend({

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

Ilekroć usuwam widok, po prostu wywołuję metodę dispose, która wyczyści wszystko automatycznie:

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

Podzieliłem się tą techniką z ludźmi, którzy piszą ebook „Backbone.js on Rails” i uważam, że jest to technika, którą przyjęli w tej książce.

Aktualizacja: 2014-03-24

Począwszy od Backone 0.9.9, ListenTo i stopListening zostały dodane do Events przy użyciu tych samych technik bindTo i unbindFromAll pokazanych powyżej. Ponadto View.remove wywołuje stopListening automatycznie, więc wiązanie i odłączanie jest teraz tak proste:

var SampleView = BaseView.extend({

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

var sampleView = new SampleView({model: some_model});
sampleView.remove();
Johnny Oshika
źródło
Czy masz jakieś sugestie, jak usunąć zagnieżdżone widoki? W tej chwili robię coś podobnego do bindTo: gist.github.com/1288947, ale myślę, że można zrobić coś lepszego.
Dmitry Polushkin
Dmitry, robię coś podobnego do tego, co robisz, aby pozbyć się zagnieżdżonych widoków. Nie widziałem jeszcze lepszego rozwiązania, ale chciałbym też wiedzieć, czy istnieje. Oto kolejna dyskusja, która również tego dotyczy: groups.google.com/forum/#!topic/backbonejs/3ZFm-lteN-A . Zauważyłem, że w Twoim rozwiązaniu nie bierzesz pod uwagę scenariusza, w którym zagnieżdżony widok jest usuwany bezpośrednio. W takim scenariuszu widok nadrzędny nadal będzie zawierał odwołanie do widoku zagnieżdżonego, mimo że widok zagnieżdżony został usunięty. Nie wiem, czy musisz to wyjaśnić.
Johnny Oshika,
Co jeśli mam funkcję, która otwiera i zamyka ten sam widok. Mam przyciski do przodu i do tyłu. Jeśli wywołam dispose, usunie to element z DOM. Czy powinienem cały czas zachowywać widok w pamięci?
dagda1
1
Cześć fisherwebdev. Możesz również użyć tej techniki z Backbone.View.extend, ale będziesz musiał zainicjować this.bindings w metodzie BaseView.initialize. Problem z tym polega na tym, że jeśli twój odziedziczony widok implementuje własną metodę inicjalizacji, będzie musiał jawnie wywołać metodę inicjalizacji BaseView. Szczegółowo wyjaśniłem ten problem tutaj: stackoverflow.com/a/7736030/188740
Johnny Oshika
2
Cześć SunnyRed, zaktualizowałem moją odpowiedź, aby lepiej odzwierciedlała mój powód niszczenia widoków. W przypadku Backbone nie widzę powodu, by kiedykolwiek ponownie ładować stronę po uruchomieniu aplikacji, więc moja pojedyncza aplikacja strony stała się dość duża. Gdy użytkownicy wchodzą w interakcję z moją aplikacją, stale renderuję różne sekcje strony (np. Przełączam się z widoku szczegółów do widoku edycji), więc o wiele łatwiej jest zawsze tworzyć nowe widoki, niezależnie od tego, czy ta sekcja była wcześniej renderowana, czy nie. Z drugiej strony modele reprezentują obiekty biznesowe, więc zmodyfikowałbym je tylko wtedy, gdy obiekt naprawdę się zmienił.
Johnny Oshika
8

To powszechny stan. Jeśli za każdym razem utworzysz nowy widok, wszystkie stare widoki będą nadal powiązane ze wszystkimi wydarzeniami. Jedną z rzeczy, które możesz zrobić, jest utworzenie w swoim widoku funkcji o nazwie detatch:

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

Następnie, zanim utworzysz nowy widok, wywołaj detatchstary widok.

Oczywiście, jak wspomniałeś, zawsze możesz utworzyć jeden widok „szczegółów” i nigdy go nie zmieniać. Możesz powiązać się ze zdarzeniem „zmiana” w modelu (z widoku), aby ponownie renderować siebie. Dodaj to do swojego inicjatora:

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

Spowoduje to ponowne renderowanie okienka szczegółów za KAŻDĄ zmianą w modelu. Możesz uzyskać większą szczegółowość, obserwując pojedynczą właściwość: „change: propName”.

Oczywiście wykonanie tego wymaga wspólnego modelu, do którego odwołuje się widok elementu, a także widoku listy wyższego poziomu i widoku szczegółów.

Mam nadzieję że to pomoże!

Brian Genisio
źródło
1
Hmmm, zrobiłem coś zgodnie z sugestiami, które sugerowałeś, ale nadal mam problemy: na przykład this.model.unbind()jest źle dla mnie, ponieważ rozwiązuje wszystkie zdarzenia z tego modelu, w tym zdarzenia dotyczące innych widoków tego samego użytkownika. Ponadto, aby wywołać detachfunkcję, muszę zachować statyczne odniesienie do widoku, a całkiem mi się to nie podoba. Podejrzewam, że wciąż jest coś, czego nie
podjąłem
6

Aby naprawić zdarzenia związane wielokrotnie,

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

Użycie powyższej linii przed utworzeniem nowych widoków z trasy rozwiązało problem, który miałem z widokami zombie.

Ashan
źródło
Jest tu wiele bardzo dobrych, szczegółowych odpowiedzi. Zdecydowanie zamierzam przyjrzeć się niektórym sugestiom ViewManger. Jednak ten był śmiertelnie prosty i działa idealnie dla mnie, ponieważ moje widoki to wszystkie panele z metodami close (), w których mogę po prostu rozpiąć zdarzenia. Dzięki Ashan
netpoetica
2
Nie mogę wyrenderować ponownie po rozpięciu: \
CodeGuru
@FlyingAtom: Nawet ja nie jestem w stanie ponownie renderować widoków po rozpakowaniu. Czy znalazłeś jakiś sposób, aby to zrobić?
Raeesaa,
zobacz. $ el.removeData (). unbind ();
Alexander Mills,
2

Myślę, że większość ludzi zaczyna od Backbone stworzy widok jak w Twoim kodzie:

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

Ten kod tworzy widok zombie, ponieważ możemy stale tworzyć nowy widok bez czyszczenia istniejącego widoku. Jednak wywołanie metody view.dispose () dla wszystkich widoków szkieletu w Twojej aplikacji nie jest wygodne (zwłaszcza jeśli tworzymy widoki w pętli for)

Myślę, że najlepszym momentem na umieszczenie kodu porządkującego jest przed utworzeniem nowego widoku. Moim rozwiązaniem jest utworzenie pomocnika do wykonania tego czyszczenia:

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

Używanie maszyny wirtualnej do tworzenia widoku pomoże wyczyścić istniejący widok bez konieczności wywoływania metody view.dispose (). Możesz dokonać niewielkiej modyfikacji swojego kodu z

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

do

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

Więc to od Ciebie zależy, czy chcesz ponownie używać widoku zamiast go ciągle tworzyć, o ile widok jest czysty, nie musisz się martwić. Po prostu zmień createView na reuseView:

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

Szczegółowy kod i atrybuty są zamieszczone na https://github.com/thomasdao/Backbone-View-Manager

thomasdao
źródło
Ostatnio intensywnie pracowałem z kręgosłupem i wydaje się, że jest to najbardziej dopracowany sposób obsługi widoków zombie podczas tworzenia lub ponownego wykorzystywania widoków. Zwykle podążam za przykładem Dericka Baileya, ale w tym przypadku wydaje się to bardziej elastyczne. Moje pytanie brzmi: dlaczego więcej osób nie używa tej techniki?
MFD3000
może dlatego, że jest ekspertem w Backbone :). Myślę, że ta technika jest dość prosta i całkiem bezpieczna w użyciu, stosowałem ją i do tej pory nie mam problemu :)
thomasdao
0

Jedną z alternatyw jest wiązanie, a nie tworzenie serii nowych widoków, a następnie usuwanie tych widoków. Osiągnąłbyś to, robiąc coś takiego:

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

Należy ustawić model myView na myViewModel, który zostałby ustawiony na model użytkownika. W ten sposób, jeśli ustawisz myViewModel na innego użytkownika (tj. Zmieniając jego atrybuty), może to wyzwolić funkcję renderującą w widoku z nowymi atrybutami.

Jednym z problemów jest to, że powoduje to zerwanie łącza do oryginalnego modelu. Można to obejść, używając obiektu kolekcji lub ustawiając model użytkownika jako atrybut modelu widoku. Wtedy byłoby to dostępne w widoku jako myview.model.get („model”).

bento
źródło
1
Globalny zasięg zanieczyszczenia nigdy nie jest dobrym pomysłem. Dlaczego miałbyś tworzyć instancje BB.Models i BB.Views w przestrzeni nazw okna?
Vernon,
0

Użyj tej metody, aby wyczyścić widoki potomne i bieżące widoki z pamięci.

//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
źródło