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

7 ответов

Решение

Недавно я написал об этом в блоге и показал несколько вещей, которые я делаю в своих приложениях для обработки этих сценариев:

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

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

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

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

Всякий раз, когда необходимо привязать представление к событию модели или коллекции, я бы использовал метод 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

Начиная с версии 0.9.9, к событиям добавлены listenTo и stopListening с использованием тех же методов 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();

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

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

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

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

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

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

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

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

Чтобы исправить привязку событий несколько раз,

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

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

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

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

Этот код создает зомби-представление, потому что мы можем постоянно создавать новое представление без очистки существующего представления. Однако вызывать view.dispose() для всех базовых представлений в вашем приложении неудобно (особенно если мы создаем представления в цикле 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'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];
}

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

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

в

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

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

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 для другого пользователя (т. Е. Измените его атрибуты), он может вызвать функцию рендеринга в представлении с новыми атрибутами.

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

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

//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);
       }
  });
Другие вопросы по тегам