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

83

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

У меня есть 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;
    }
})
солендил
источник

Ответы:

28

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

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

Дерик Бейли
источник
1
Почему не только delete viewв роутере?
Trantor Liu
Я поддержал ваш ответ, но было бы полезно, если бы соответствующие части сообщения в блоге были внутри самого ответа, поскольку это цель здесь.
Эмиль Бержерон
136

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

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

Сначала я создаю 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;

Всякий раз, когда 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 были добавлены к событиям с использованием тех же методов 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();
Джонни Ошика
источник
Есть ли у вас какие-либо предложения по удалению вложенных представлений? Прямо сейчас я делаю то же, что и bindTo: gist.github.com/1288947, но я думаю, что можно сделать что-то еще лучше.
Дмитрий Полушкин
Дмитрий, я делаю что-то похожее на то, что вы делаете, для удаления вложенных представлений. Я еще не видел лучшего решения, но мне также было бы интересно узнать, есть ли оно. Вот еще одно обсуждение, которое также касается этого: groups.google.com/forum/#!topic/backbonejs/3ZFm-lteN-A . Я заметил, что в вашем решении вы не учитываете сценарий, когда вложенное представление удаляется напрямую. В таком сценарии родительское представление по-прежнему будет содержать ссылку на вложенное представление, даже если вложенное представление удалено. Я не знаю, нужно ли вам это объяснять.
Джонни Ошика
Что делать, если у меня есть функция, открывающая и закрывающая одно и то же представление. У меня есть кнопки вперед и назад. Если я вызову dispose, он удалит элемент из DOM. Должен ли я все время держать вид в памяти?
dagda1 01
1
Привет, fisherwebdev. Вы также можете использовать этот метод с Backbone.View.extend, но вам нужно будет инициализировать this.bindings в методе BaseView.initialize. Проблема заключается в том, что если ваше унаследованное представление реализует свой собственный метод инициализации, то ему нужно будет явно вызвать метод инициализации BaseView. Я объяснил эту проблему более подробно здесь: stackoverflow.com/a/7736030/188740
Джонни Ошика
2
Привет, SunnyRed, я обновил свой ответ, чтобы лучше отразить мою причину уничтожения просмотров. С Backbone я не вижу причин перезагружать страницу после запуска приложения, поэтому мое одностраничное приложение стало довольно большим. По мере того как пользователи взаимодействуют с моим приложением, я постоянно меняю рендеринг различных разделов страницы (например, переключаюсь с детального представления на режим редактирования), поэтому мне намного проще всегда создавать новые представления, независимо от того, был ли этот раздел отрисован ранее или не. С другой стороны, модели представляют бизнес-объекты, поэтому я бы изменил их только в том случае, если объект действительно изменился.
Джонни Ошика
8

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

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

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

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

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

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

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

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

Брайан Генизио
источник
1
Хммм, я сделал что-то в том же духе, что и вы, но у меня все еще есть проблемы: например, this.model.unbind()это неправильно для меня, потому что он отключает все события из этой модели, включая события, относящиеся к другим представлениям того же пользователя. Более того, чтобы вызвать detachфункцию, мне нужно сохранить статическую ссылку на представление, и мне это совсем не нравится. Я подозреваю, что есть еще кое-что, чего я еще не понял ...
солендил
6

Чтобы исправить многократную привязку событий,

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

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

Ашан
источник
Здесь есть много очень хороших и подробных ответов. Я определенно намерен изучить некоторые предложения ViewManger. Однако этот был очень простым и отлично работал для меня, потому что все мои представления - это панели с методами close (), где я могу просто отвязать события. Спасибо, Ашан
netpoetica
2
Кажется, я не могу повторно выполнить рендеринг после того, как отвязал привязку: \
CodeGuru
@FlyingAtom: Даже я не могу повторно визуализировать представления после отмены привязки. Вы нашли способ сделать это?
Raeesaa
просмотр. $ el.removeData (). unbind ();
Александр Миллс,
2

Я думаю, что большинство людей, начинающих с 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'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];
}

Использование виртуальной машины для создания вашего представления поможет очистить любое существующее представление без необходимости вызывать 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.

Томасдао
источник
В последнее время я активно работал с backbone, и это, кажется, наиболее развитое средство обработки зомби-представлений при построении или повторном использовании представлений. Обычно я следую примерам Дерика Бейли, но в данном случае это кажется более гибким. Мой вопрос: почему все больше людей не используют эту технику?
MFD3000
может потому, что он эксперт в Backbone :). Я думаю, что эта техника довольно проста и вполне безопасна в использовании, я ее использовал, и пока у меня нет проблем :)
thomasdao
0

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

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 («модель»).

Bento
источник
1
Загрязнение глобального масштаба - плохая идея. Зачем создавать экземпляры BB.Models и BB.Views в пространстве имен окна?
Вернон
0

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

//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);
       }
  });
Робинс Гупта
источник