Backbone.js: repovoar ou recriar a visualização?

83

Em meu aplicativo da web, tenho uma lista de usuários em uma tabela à esquerda e um painel de detalhes do usuário à direita. Quando o administrador clica em um usuário na tabela, seus detalhes devem ser exibidos à direita.

Eu tenho um UserListView e UserRowView à esquerda e um UserDetailView à direita. As coisas funcionam, mas tenho um comportamento estranho. Se eu clicar em alguns usuários à esquerda e, em seguida, clicar em excluir em um deles, obtenho caixas de confirmação de javascript sucessivas para todos os usuários que foram exibidos.

Parece que as associações de eventos de todas as visualizações exibidas anteriormente não foram removidas, o que parece ser normal. Não devo fazer um novo UserDetailView todas as vezes no UserRowView? Devo manter uma visualização e alterar seu modelo de referência? Devo controlar a visualização atual e removê-la antes de criar uma nova? Estou meio perdido e qualquer ideia será bem-vinda. Obrigado !

Aqui está o código da visão esquerda (exibição de linha, evento de clique, criação de visão direita)

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

E o código para a visualização correta (botão excluir)

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
fonte

Respostas:

136

Sempre destruo e crio visualizações porque, à medida que meu aplicativo de página única fica cada vez maior, seria difícil manter as visualizações ao vivo não utilizadas na memória apenas para poder reutilizá-las.

Aqui está uma versão simplificada de uma técnica que uso para limpar minhas visualizações para evitar vazamentos de memória.

Primeiro crio um BaseView do qual todas as minhas visualizações herdam. A ideia básica é que minha View manterá uma referência a todos os eventos aos quais está inscrita, de modo que, quando for hora de descartar a View, todas essas ligações serão automaticamente desassociadas. Aqui está um exemplo de implementação do meu 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;

Sempre que uma View precisa se vincular a um evento em um modelo ou coleção, eu usaria o método bindTo. Por exemplo:

var SampleView = BaseView.extend({

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

Sempre que removo uma visualização, apenas chamo o método dispose, que limpará tudo automaticamente:

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

Eu compartilhei essa técnica com o pessoal que está escrevendo o ebook "Backbone.js on Rails" e acredito que essa é a técnica que eles adotaram para o livro.

Atualização: 24/03/2014

A partir do Backone 0.9.9, listenTo e stopListening foram adicionados aos eventos usando as mesmas técnicas bindTo e unbindFromAll mostradas acima. Além disso, View.remove chama stopListening automaticamente, então vincular e desvincular é tão fácil quanto isso agora:

var SampleView = BaseView.extend({

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

var sampleView = new SampleView({model: some_model});
sampleView.remove();
Johnny Oshika
fonte
Você tem alguma sugestão de como descartar visualizações aninhadas? No momento, estou fazendo algo semelhante ao bindTo: gist.github.com/1288947, mas acho que é possível fazer algo mais melhor.
Dmitry Polushkin,
Dmitry, eu faço algo semelhante ao que você está fazendo para descartar visualizações aninhadas. Ainda não vi uma solução melhor, mas também gostaria de saber se existe uma. Aqui está outra discussão que aborda isso também: groups.google.com/forum/#!topic/backbonejs/3ZFm-lteN-A . Percebi que, em sua solução, você não está levando em consideração o cenário em que uma visão aninhada é descartada diretamente. Nesse cenário, a visão pai ainda manterá uma referência à visão aninhada, mesmo que a visão aninhada seja descartada. Não sei se você precisa dar conta disso.
Johnny Oshika,
E se eu tiver uma funcionalidade que abre e fecha a mesma visualização. Eu tenho botões para frente e para trás. Se eu chamar dispose, ele removerá o elemento do DOM. Devo manter a vista na memória o tempo todo?
dagda1 01 de
1
Olá, fisherwebdev. Você também pode usar essa técnica com Backbone.View.extend, mas precisará inicializar this.bindings no método BaseView.initialize. O problema com isso é que, se sua visão herdada implementar seu próprio método de inicialização, ela precisará chamar explicitamente o método de inicialização de BaseView. Expliquei esse problema com mais detalhes aqui: stackoverflow.com/a/7736030/188740
Johnny Oshika
2
Olá SunnyRed. Atualizei minha resposta para refletir melhor meu motivo para destruir visualizações. Com o Backbone, não vejo razão para recarregar uma página depois que um aplicativo é iniciado, então meu aplicativo de página única ficou muito grande. Conforme os usuários interagem com meu aplicativo, estou constantemente renderizando diferentes seções da página (por exemplo, alternando da visualização de detalhes para a de edição), então acho muito mais fácil sempre criar novas visualizações, independentemente de a seção ter sido renderizada anteriormente ou não. Os modelos, por outro lado, representam objetos de negócios, então eu só os modificaria se o objeto realmente mudasse.
Johnny Oshika
8

Esta é uma condição comum. Se você criar uma nova visualização todas as vezes, todas as visualizações antigas ainda estarão vinculadas a todos os eventos. Uma coisa que você pode fazer é criar uma função em sua visualização chamada detatch:

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

Então, antes de criar a nova visualização, certifique-se de chamar detatcha visualização antiga.

Claro, como você mencionou, você sempre pode criar uma visualização de "detalhe" e nunca alterá-la. Você pode vincular ao evento "alterar" no modelo (da vista) para renderizar novamente. Adicione isto ao seu inicializador:

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

Isso fará com que o painel de detalhes seja renderizado novamente CADA vez que uma alteração for feita no modelo. Você pode obter uma granularidade mais precisa observando uma única propriedade: "change: propName".

Obviamente, fazer isso requer um modelo comum ao qual o item View faz referência, bem como a visualização de lista de nível superior e a visualização de detalhes.

Espero que isto ajude!

Brian Genisio
fonte
1
Hmmm, eu fiz algo nos moldes que você sugeriu, mas ainda tenho problemas: por exemplo, o this.model.unbind()é errado para mim porque desassocia todos os eventos deste modelo, incluindo eventos relacionados a outras visualizações do mesmo usuário. Além disso, para chamar a detachfunção, preciso manter uma referência estática para a visualização e não gosto disso. Eu suspeito que ainda há algo que eu não entendi ...
solendil
6

Para corrigir eventos vinculados várias vezes,

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

Usando a linha acima antes de instanciar as novas visualizações da rota, resolvi o problema que eu tinha com visualizações zumbis.

Ashan
fonte
Existem muitas respostas muito boas e detalhadas aqui. Definitivamente, pretendo examinar algumas das sugestões do ViewManger. No entanto, este era muito simples e funciona perfeitamente para mim porque minhas visualizações são todas painéis com métodos close (), onde posso apenas desvincular os eventos. Obrigado
Ashan
2
Não consigo renderizar novamente depois de desvincular: \
CodeGuru
@FlyingAtom: Mesmo eu não estou sendo capaz de renderizar novamente as visualizações após desvincular. Você encontrou alguma maneira de fazer isso?
Raeesaa de
visualizar. $ el.removeData (). unbind ();
Alexander Mills
2

Acho que a maioria das pessoas começa com o Backbone criará a visualização como no seu código:

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

Este código cria uma visão zumbi, porque podemos criar constantemente uma nova visão sem limpar a visão existente. No entanto, não é conveniente chamar view.dispose () para todas as visualizações de backbone em seu aplicativo (especialmente se criarmos visualizações em loop for)

Acho que o melhor momento para colocar o código de limpeza é antes de criar uma nova visualização. Minha solução é criar um ajudante para fazer esta limpeza:

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

Usar o VM para criar sua visão ajudará a limpar qualquer visão existente sem ter que chamar view.dispose (). Você pode fazer uma pequena modificação em seu código de

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

para

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

Portanto, depende de você se deseja reutilizar a visualização em vez de criá-la constantemente, desde que a visualização esteja limpa, você não precisa se preocupar. Basta alterar createView para reuseView:

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

O código detalhado e a atribuição estão postados em https://github.com/thomasdao/Backbone-View-Manager

Thomasdao
fonte
Tenho trabalhado extensivamente com backbone ultimamente e este parece ser o meio mais desenvolvido de lidar com visualizações zumbis ao construir ou reutilizar visualizações. Normalmente sigo os exemplos de Derick Bailey, mas, neste caso, parece mais flexível. Minha pergunta é: por que não há mais pessoas usando essa técnica?
MFD3000
talvez porque ele seja especialista em Backbone :). Acho que essa técnica é bastante simples e segura de usar, tenho usado e não tive problemas até agora :)
thomasdao
0

Uma alternativa é vincular, em vez de criar uma série de novas visualizações e, em seguida, desvincular essas visualizações. Você faria isso fazendo algo como:

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

Você definiria o modelo de myView como myViewModel, que seria definido como um modelo de usuário. Dessa forma, se você definir myViewModel para outro usuário (ou seja, alterando seus atributos), ele poderá acionar uma função de renderização na visualização com os novos atributos.

Um problema é que isso quebra o vínculo com o modelo original. Você pode contornar isso usando um objeto de coleção ou definindo o modelo do usuário como um atributo do modelo de visão. Então, ele estaria acessível na visualização como myview.model.get ("model").

bento
fonte
1
Poluir o escopo global nunca é uma boa ideia. Por que você instanciaria BB.Models e BB.Views no namespace da janela?
Vernon
0

Use este método para limpar as visualizações secundárias e atuais da memória.

//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
fonte