web-dev-qa-db-fra.com

Backbone.js: repeupler ou recréer la vue?

Dans mon application Web, j'ai une liste d'utilisateurs dans un tableau à gauche et un volet de détails utilisateur à droite. Lorsque l'administrateur clique sur un utilisateur dans le tableau, ses détails doivent être affichés à droite.

J'ai un UserListView et UserRowView sur la gauche, et un UserDetailView sur la droite. Les choses fonctionnent, mais j'ai un comportement bizarre. Si je clique sur certains utilisateurs à gauche, puis sur Supprimer sur l'un d'eux, j'obtiens des cases de confirmation javascript successives pour tous les utilisateurs qui ont été affichés.

Il semble que les liaisons d'événements de toutes les vues précédemment affichées n'aient pas été supprimées, ce qui semble normal. Je ne devrais pas faire une nouvelle UserDetailView à chaque fois sur UserRowView? Dois-je conserver une vue et changer son modèle de référence? Dois-je garder la vue actuelle et la supprimer avant d'en créer une nouvelle? Je suis un peu perdu et toute idée sera la bienvenue. Je vous remercie !

Voici le code de la vue de gauche (affichage des lignes, événement click, création vue droite)

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

Et le code pour la vue de droite (bouton supprimer)

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;
    }
})
82
solendil

J'ai récemment blogué à ce sujet et j'ai montré plusieurs choses que je fais dans mes applications pour gérer ces scénarios:

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

27
Derick Bailey

Je détruis et crée toujours des vues, car à mesure que mon application d'une seule page s'agrandit, il est difficile de conserver les vues en direct inutilisées afin de pouvoir les réutiliser.

Voici une version simplifiée d'une technique que j'utilise pour nettoyer mes vues pour éviter les fuites de mémoire.

Je crée d'abord une BaseView dont toutes mes vues héritent. L'idée de base est que ma vue conservera une référence à tous les événements auxquels elle est abonnée, de sorte que lorsqu'il sera temps de supprimer la vue, toutes ces liaisons seront automatiquement non liées. Voici un exemple d'implémentation de ma 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;

Chaque fois qu'une vue doit se lier à un événement sur un modèle ou une collection, j'utilise la méthode bindTo. Par exemple:

var SampleView = BaseView.extend({

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

Chaque fois que je supprime une vue, j'appelle simplement la méthode dispose qui nettoiera tout automatiquement:

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

J'ai partagé cette technique avec les gens qui écrivent l'ebook "Backbone.js on Rails" et je crois que c'est la technique qu'ils ont adoptée pour le livre.

Mise à jour: 2014-03-24

À partir de Backone 0.9.9, listenTo et stopListening ont été ajoutés aux événements en utilisant les mêmes techniques bindTo et unbindFromAll indiquées ci-dessus. De plus, View.remove appelle automatiquement stopListening, donc la liaison et la dissociation sont aussi faciles que cela maintenant:

var SampleView = BaseView.extend({

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

var sampleView = new SampleView({model: some_model});
sampleView.remove();
136
Johnny Oshika

Ceci est une condition courante. Si vous créez une nouvelle vue à chaque fois, toutes les anciennes vues seront toujours liées à tous les événements. Une chose que vous pouvez faire est de créer une fonction sur votre vue appelée detatch:

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

Ensuite, avant de créer la nouvelle vue, assurez-vous d'appeler detatch sur l'ancienne vue.

Bien sûr, comme vous l'avez mentionné, vous pouvez toujours créer une vue "détaillée" et ne jamais la modifier. Vous pouvez vous lier à l'événement "change" sur le modèle (depuis la vue) pour vous restituer. Ajoutez ceci à votre initialiseur:

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

Cela entraînera le rendu du volet d'informations à CHAQUE fois qu'une modification est apportée au modèle. Vous pouvez obtenir une granularité plus fine en recherchant une seule propriété: "change: propName".

Bien sûr, cela nécessite un modèle commun auquel la vue d'élément fait référence ainsi que la vue de liste de niveau supérieur et la vue de détails.

J'espère que cela t'aides!

8
Brian Genisio

Pour corriger la liaison d'événements plusieurs fois,

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

L'utilisation de la ligne ci-dessus avant d'instancier les nouvelles vues de l'itinéraire a résolu le problème que j'avais avec les vues zombies.

6
Ashan

Je pense que la plupart des gens commencent par Backbone créeront la vue comme dans votre code:

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

Ce code crée une vue zombie, car nous pouvons constamment créer une nouvelle vue sans nettoyer la vue existante. Cependant, il n'est pas pratique d'appeler view.dispose () pour toutes les vues de base de votre application (surtout si nous créons des vues dans la boucle for)

Je pense que le meilleur moment pour mettre du code de nettoyage est avant de créer une nouvelle vue. Ma solution est de créer un assistant pour effectuer ce nettoyage:

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

L'utilisation de VM pour créer votre vue aidera à nettoyer toute vue existante sans avoir à appeler view.dispose (). Vous pouvez apporter une petite modification à votre code à partir de

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

à

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

Donc, c'est à vous de décider si vous souhaitez réutiliser la vue au lieu de la créer constamment, tant que la vue est propre, vous n'avez pas à vous inquiéter. Changez simplement createView pour réutiliserView:

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

Le code détaillé et l'attribution sont publiés sur https://github.com/thomasdao/Backbone-View-Manager

2
thomasdao

Une alternative consiste à lier, au lieu de créer une série de nouvelles vues, puis de dissocier ces vues. Vous accomplirez cela en faisant quelque chose comme:

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

Vous devez définir le modèle de myView sur myViewModel, qui serait défini sur un modèle utilisateur. De cette façon, si vous définissez myViewModel sur un autre utilisateur (c'est-à-dire en modifiant ses attributs), cela pourrait déclencher une fonction de rendu dans la vue avec les nouveaux attributs.

Un problème est que cela rompt le lien avec le modèle d'origine. Vous pouvez contourner ce problème en utilisant un objet de collection ou en définissant le modèle utilisateur comme attribut du modèle de vue. Ensuite, cela serait accessible dans la vue sous le nom myview.model.get ("modèle").

0
bento

tilisez cette méthode pour effacer les vues enfant et les vues actuelles de la mémoire.

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