82 votes

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

Dans mon application web, j'ai une liste d'utilisateurs dans un tableau sur la gauche, et un volet de détails sur les utilisateurs sur la droite. Lorsque l'administrateur clique sur un utilisateur dans le tableau, ses détails doivent s'afficher sur la droite.

J'ai un UserListView et un UserRowView sur la gauche, et un UserDetailView sur la droite. Les choses fonctionnent à peu près, mais j'ai un comportement bizarre. Si je clique sur certains utilisateurs à gauche, puis sur supprimer l'un d'entre eux, j'obtiens des boîtes de confirmation successives en javascript 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'ont pas été supprimées, ce qui semble être normal. Je ne devrais pas créer un nouveau UserDetailView à chaque fois sur UserRowView ? Dois-je maintenir une vue et changer son modèle de référence ? Devrais-je garder la trace de 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. Merci !

Voici le code de la vue de gauche (affichage des lignes, événement de clic, création de la vue de 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 de suppression)

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

136voto

Johnny Oshika Points 15580

Je détruis et crée toujours des vues parce qu'au fur et à mesure que mon application à page unique devient de plus en plus grande, garder en mémoire des vues vivantes inutilisées juste pour pouvoir les réutiliser deviendrait difficile à maintenir.

Voici une version simplifiée d'une technique que j'utilise pour nettoyer mes vues afin d'é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 conserve une référence à tous les événements auxquels elle est abonnée, de sorte que lorsqu'il est temps de disposer de la vue, tous ces liens sont automatiquement supprimés. 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;

Lorsqu'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);
    }
});

Lorsque je supprime une vue, j'appelle simplement la méthode dispose qui nettoie tout automatiquement :

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

J'ai partagé cette technique avec les personnes qui rédigent 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 présentées ci-dessus. En outre, View.remove appelle automatiquement stopListening, de sorte que la liaison et la séparation sont désormais aussi simples que cela :

var SampleView = BaseView.extend({

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

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

27voto

Derick Bailey Points 37859

J'ai récemment publié un blog à 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/

8voto

Brian Genisio Points 30777

C'est un problème courant. 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étail" et ne jamais la modifier. Vous pouvez vous lier à l'événement "change" sur le modèle (à partir de la vue) pour effectuer un nouveau rendu. Ajoutez ceci à votre initialisateur :

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

Si vous faites cela, le volet des détails sera rendu à nouveau CHAQUE fois qu'une modification est apportée au modèle. Vous pouvez obtenir une granularité plus fine en surveillant une seule propriété : "change:propName".

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

J'espère que cela vous aidera !

6voto

Ashan Points 119

Pour fixer les événements qui se lient plusieurs fois,

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

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

2voto

thomasdao Points 1766

Je pense que la plupart des gens qui commencent avec Backbone vont créer 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 Backbone de votre application (surtout si nous créons des vues dans une boucle for).

Je pense que le meilleur moment pour mettre le code de nettoyage est avant de créer une nouvelle vue. Ma solution est de créer une aide pour faire 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 permettra de nettoyer toute vue existante sans avoir à appeler view.dispose(). Vous pouvez faire une petite modification à votre code de

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

à

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

C'est donc à vous de décider si vous voulez réutiliser la vue au lieu de la créer constamment, tant que la vue est propre, vous n'avez pas à vous inquiéter. Il suffit de remplacer createView par reuseView :

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

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X