Backbone.js: ¿repoblar o recrear la vista?

83

En mi aplicación web, tengo una lista de usuarios en una tabla a la izquierda y un panel de detalles de usuarios a la derecha. Cuando el administrador hace clic en un usuario en la tabla, sus detalles deben mostrarse a la derecha.

Tengo un UserListView y UserRowView a la izquierda, y un UserDetailView a la derecha. Las cosas funcionan, pero tengo un comportamiento extraño. Si hago clic en algunos usuarios a la izquierda, luego hago clic en eliminar en uno de ellos, obtengo cuadros de confirmación de javascript sucesivos para todos los usuarios que se han mostrado.

Parece que los enlaces de eventos de todas las vistas mostradas anteriormente no se han eliminado, lo que parece ser normal. ¿No debería hacer un nuevo UserDetailView cada vez en UserRowView? ¿Debo mantener una vista y cambiar su modelo de referencia? ¿Debo realizar un seguimiento de la vista actual y eliminarla antes de crear una nueva? Estoy un poco perdido y cualquier idea será bienvenida. Gracias !

Aquí está el código de la vista izquierda (visualización de fila, evento de clic, creación de vista derecha)

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

Y el código para la vista derecha (botón eliminar)

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
fuente

Respuestas:

136

Siempre destruyo y creo vistas porque a medida que mi aplicación de una sola página se hace cada vez más grande, mantener las vistas en vivo no utilizadas en la memoria solo para poder reutilizarlas sería difícil de mantener.

Aquí hay una versión simplificada de una técnica que utilizo para limpiar mis Vistas y evitar pérdidas de memoria.

Primero creo una BaseView de la que heredan todas mis vistas. La idea básica es que mi Vista mantendrá una referencia a todos los eventos a los que está suscrito, de modo que cuando sea el momento de deshacerse de la Vista, todos esos enlaces se desvincularán automáticamente. Aquí hay una implementación de ejemplo de mi 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;

Siempre que una vista necesita vincularse a un evento en un modelo o colección, usaría el método bindTo. Por ejemplo:

var SampleView = BaseView.extend({

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

Siempre que elimino una vista, simplemente llamo al método de disposición que limpiará todo automáticamente:

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

Compartí esta técnica con las personas que están escribiendo el libro electrónico "Backbone.js on Rails" y creo que esta es la técnica que han adoptado para el libro.

Actualización: 2014-03-24

A partir de Backone 0.9.9, listenTo y stopListening se agregaron a los eventos usando las mismas técnicas bindTo y unbindFromAll que se muestran arriba. Además, View.remove llama a stopListening automáticamente, por lo que vincular y desvincular es tan fácil como esto ahora:

var SampleView = BaseView.extend({

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

var sampleView = new SampleView({model: some_model});
sampleView.remove();
Johnny Oshika
fuente
¿Tiene alguna sugerencia sobre cómo eliminar las vistas anidadas? En este momento estoy haciendo algo similar al bindTo: gist.github.com/1288947 pero supongo que es posible hacer algo mejor.
Dmitry Polushkin
Dmitry, hago algo similar a lo que estás haciendo para eliminar las vistas anidadas. Todavía no he visto una solución mejor, pero también me interesaría saber si existe. Aquí hay otra discusión que también toca esto: groups.google.com/forum/#!topic/backbonejs/3ZFm-lteN-A . Noté que en su solución, no está teniendo en cuenta el escenario en el que una vista anidada se elimina directamente. En tal escenario, la vista principal seguirá teniendo una referencia a la vista anidada aunque se elimine la vista anidada. No sé si necesita dar cuenta de esto.
Johnny Oshika
¿Qué sucede si tengo una funcionalidad que abre y cierra la misma vista? Tengo botones de avance y retroceso. Si llamo a dispose, eliminará el elemento del DOM. ¿Debo mantener la vista en la memoria todo el tiempo?
dagda1
1
Hola, fisherwebdev. También puede utilizar esta técnica con Backbone.View.extend, pero deberá inicializar this.bindings en el método BaseView.initialize. El problema con esto es que si su vista heredada implementa su propio método de inicialización, entonces deberá llamar explícitamente al método de inicialización de BaseView. Expliqué este problema con más detalle aquí: stackoverflow.com/a/7736030/188740
Johnny Oshika
2
Hola SunnyRed, actualicé mi respuesta para reflejar mejor mi razón para destruir vistas. Con Backbone, no veo ninguna razón para volver a cargar una página después de que se inicia una aplicación, por lo que mi aplicación de una sola página se ha vuelto bastante grande. A medida que los usuarios interactúan con mi aplicación, estoy constantemente renderizando diferentes secciones de la página (por ejemplo, cambiando de la vista de detalles a la vista de edición), por lo que me resulta mucho más fácil crear siempre nuevas vistas, independientemente de si esa sección se renderizó previamente o no. Los modelos, por otro lado, representan objetos comerciales, por lo que solo los modificaría si el objeto realmente cambiara.
Johnny Oshika
8

Ésta es una condición común. Si crea una nueva vista cada vez, todas las vistas antiguas seguirán vinculadas a todos los eventos. Una cosa que puede hacer es crear una función en su vista llamada detatch:

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

Luego, antes de crear la nueva vista, asegúrese de llamar detatcha la vista anterior.

Por supuesto, como mencionaste, siempre puedes crear una vista de "detalle" y nunca cambiarla. Puede vincularse al evento "cambiar" en el modelo (desde la vista) para volver a renderizarlo usted mismo. Agregue esto a su inicializador:

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

Hacer eso hará que el panel de detalles se vuelva a renderizar CADA vez que se realice un cambio en el modelo. Puede obtener una granularidad más fina observando una sola propiedad: "change: propName".

Por supuesto, hacer esto requiere un modelo común al que tenga referencia la Vista de elementos, así como la vista de lista de nivel superior y la vista de detalles.

¡Espero que esto ayude!

Brian Genisio
fuente
1
Hmmm, hice algo similar a lo que sugirió, pero todavía tengo problemas: por ejemplo, no this.model.unbind()es correcto para mí porque desvincula todos los eventos de este modelo, incluidos los eventos relacionados con otras vistas del mismo usuario. Además, para llamar a la detachfunción, necesito mantener una referencia estática a la vista, y no me gusta. Sospecho que todavía hay algo que no he entendido ...
solendil
6

Para arreglar eventos vinculantes varias veces,

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

Usando la línea anterior antes de crear una instancia de las nuevas Vistas desde la ruta, resolvió el problema que tenía con las vistas de zombies.

Ashán
fuente
Aquí hay muchas respuestas muy buenas y detalladas. Definitivamente tengo la intención de analizar algunas de las sugerencias de ViewManger. Sin embargo, este fue muy simple y funciona perfectamente para mí porque mis Vistas son todos Paneles con métodos close (), donde puedo desvincular los eventos. Gracias Ashan
netpoetica
2
Parece que no puedo volver a renderizar después de desvincularme: \
CodeGuru
@FlyingAtom: Incluso yo no puedo volver a renderizar las vistas después de desvincularlas. ¿Encontraste alguna forma de hacer eso?
Raeesaa
ver. $ el.removeData (). unbind ();
Alexander Mills
2

Creo que la mayoría de las personas que comienzan con Backbone crearán la vista como en su código:

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

Este código crea una vista zombie, porque es posible que creemos constantemente una nueva vista sin limpiar la vista existente. Sin embargo, no es conveniente llamar a view.dispose () para todas las vistas troncales en su aplicación (especialmente si creamos vistas en bucle for)

Creo que el mejor momento para poner el código de limpieza es antes de crear una nueva vista. Mi solución es crear un ayudante para hacer esta limpieza:

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 VM para crear su vista ayudará a limpiar cualquier vista existente sin tener que llamar a view.dispose (). Puede hacer una pequeña modificación a su código desde

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

a

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

Por lo tanto, depende de usted si desea reutilizar la vista en lugar de crearla constantemente, siempre que la vista esté limpia, no debe preocuparse. Simplemente cambie createView para reuseView:

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

El código detallado y la atribución se publican en https://github.com/thomasdao/Backbone-View-Manager

Thomasdao
fuente
Últimamente he estado trabajando mucho con backbone y este parece ser el medio más desarrollado para manejar las vistas de zombies al crear o reutilizar vistas. Normalmente sigo los ejemplos de Derick Bailey, pero en este caso, esto parece más flexible. Mi pregunta es, ¿por qué no hay más gente usando esta técnica?
MFD3000
tal vez porque es experto en Backbone :). Creo que esta técnica es bastante simple y bastante segura de usar, la he estado usando y no tengo ningún problema hasta ahora :)
thomasdao
0

Una alternativa es vincular, en lugar de crear una serie de vistas nuevas y luego desvincular esas vistas. Lograrías esto haciendo 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"); 
    }
}); 

Establecería el modelo de myView en myViewModel, que se establecería en un modelo de usuario. De esta manera, si configura myViewModel a otro usuario (es decir, si cambia sus atributos), podría activar una función de renderizado en la vista con los nuevos atributos.

Un problema es que esto rompe el vínculo con el modelo original. Puede evitar esto utilizando un objeto de colección o configurando el modelo de usuario como un atributo del modelo de vista. Entonces, esto sería accesible en la vista como myview.model.get ("modelo").

bento
fuente
1
Contaminar el alcance global nunca es una buena idea. ¿Por qué crearía una instancia de BB.Models y BB.Views en el espacio de nombres de la ventana?
Vernon
0

Utilice este método para borrar las vistas secundarias y las vistas actuales de la memoria.

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