Backbone.js: повторить или воссоздать представление?

В моем веб-приложении у меня есть список пользователей в таблице слева и панель сведений о пользователе справа. Когда администратор нажимает на пользователя в таблице, его данные должны отображаться справа.

У меня есть UserListView и UserRowView слева, а UserDetailView справа. Вещи вроде работы, но у меня странное поведение. Если я нажму несколько пользователей слева, а затем нажмите "Удалить" на одном из них, я получаю следующие флажки подтверждения javascript для всех пользователей, которые были отображены.

Похоже, что привязки событий ко всем ранее отображаемым представлениям не удалены, что кажется нормальным. Я не должен делать новый UserDetailView каждый раз в UserRowView? Должен ли я поддерживать точку зрения и менять ее эталонную модель? Должен ли я отслеживать текущий вид и удалять его перед созданием нового? Я немного потерян, и любая идея будет приветствоваться. Спасибо!

Вот код левого представления (отображение строки, событие щелчка, создание правого представления)

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

И код для правильного просмотра (кнопка удаления)

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

Ответ 2

Я всегда уничтожаю и создаю представления, потому что, поскольку мое приложение для одной страницы становится все больше и больше, сохранение неиспользуемых живых представлений в памяти просто для того, чтобы я мог повторно использовать их, стало бы трудно поддерживать.

Вот упрощенная версия метода, который я использую для очистки моих просмотров, чтобы избежать утечек памяти.

Сначала создаю BaseView, из которого наследуются все мои представления. Основная идея заключается в том, что мой просмотр будет содержать ссылку на все события, на которые он подписан, так что, когда время для удаления представления, все эти привязки автоматически будут несвязаны. Вот пример реализации моего 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;

Всякий раз, когда View должен привязываться к событию модели или коллекции, я бы использовал метод bindTo. Например:

var SampleView = BaseView.extend({

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

Всякий раз, когда я удаляю представление, я просто вызываю метод dispose, который автоматически очистят все:

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

Я поделился этой техникой с людьми, которые пишут книгу "Backbone.js on Rails", и я считаю, что это техника, которую они приняли для книги.

Обновление: 2014-03-24

Начиная с Backone 0.9.9, listenTo и stopListening были добавлены в Events, используя те же методы bindTo и unbindFromAll, показанные выше. Кроме того, View.remove вызывает stopListening автоматически, поэтому привязка и развязка так же просто, как сейчас:

var SampleView = BaseView.extend({

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

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

Ответ 3

Это обычное условие. Если вы каждый раз создаете новое представление, все старые представления все равно будут привязаны ко всем событиям. Одна вещь, которую вы можете сделать, это создать функцию в вашем представлении под названием detatch:

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

Затем перед созданием нового представления обязательно вызовите detatch на старом представлении.

Конечно, как вы уже упоминали, вы всегда можете создать один "подробный" вид и никогда не изменять его. Вы можете привязываться к событию "change" на модели (с точки зрения), чтобы повторно отобразить себя. Добавьте это в свой инициализатор:

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

Выполнение этого приведет к тому, что панель сведений повторно отобразит КАЖДОЕ время, когда в модель будет внесено изменение. Вы можете получить более тонкую детализацию, наблюдая за одним свойством: "change: propName".

Конечно, для этого требуется общая модель, на которую ссылается элемент View, а также вид списка более высокого уровня и представление деталей.

Надеюсь, это поможет!

Ответ 4

Чтобы фиксировать события, связывающие несколько раз,

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

Используя вышеприведенную строку перед созданием новых представлений из маршрута, решила проблему, с которой я столкнулся с зомби-представлениями.

Ответ 5

Я думаю, что большинство людей, начиная с Backbone, создадут представление как в вашем коде:

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

Этот код создает зомби-представление, потому что мы можем постоянно создавать новый вид без очистки существующего представления. Однако это не удобно для вызова view.dispose() для всех Backbone Views в вашем приложении (особенно, если мы создаем представления в цикле for)

Я думаю, что лучшее время для установки кода очистки - это создать новый вид. Мое решение - создать помощника для этой очистки:

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

Использование виртуальной машины для создания вашего вида поможет очистить любое существующее представление без вызова функции view.dispose(). Вы можете сделать небольшую модификацию своего кода из

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

to

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

Таким образом, это зависит от вас, если вы хотите повторно использовать представление, а не постоянно его создавать, если представление чистое, вам не нужно беспокоиться. Просто измените createView на reuseView:

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

Подробный код и атрибуция размещены на https://github.com/thomasdao/Backbone-View-Manager

Ответ 6

Один из вариантов заключается в привязке, в отличие от создания ряда новых представлений, а затем открепления этих представлений. Вы выполнили бы это, например:

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

Вы должны установить модель myView в myViewModel, которая будет установлена ​​в модель пользователя. Таким образом, если вы установите myViewModel другому пользователю (т.е. Измените его атрибуты), тогда он может вызвать функцию рендеринга в представлении с новыми атрибутами.

Одна из проблем заключается в том, что это нарушает связь с исходной моделью. Вы можете обойти это, используя объект коллекции, или установив модель пользователя как атрибут viewmodel. Тогда это будет доступно в представлении как myview.model.get( "model" ).

Ответ 7

Используйте этот метод для очистки дочерних представлений и текущих представлений из памяти.

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