Как обрабатывать инициализацию и рендеринг подпредставлений в Backbone.js?

199

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


Сценарий первый:

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

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.render().appendTo(this.$('.container-placeholder');
}

Проблемы:

  • Самая большая проблема заключается в том, что повторный вызов render для родителя удалит все привязки дочерних событий. (Это из-за того, как $.html()работает jQuery .) Это можно было бы смягчить, вызвав this.child.delegateEvents().render().appendTo(this.$el);вместо этого вызов , но в первом и наиболее частом случае вы выполняете больше работы без необходимости.

  • Добавляя потомков, вы заставляете функцию рендера знать структуру DOM родителей, чтобы получить нужный порядок. Это означает, что изменение шаблона может потребовать обновления функции визуализации представления.


Сценарий второй:

Инициализируйте дочерние элементы в родительском initialize()буфере, но вместо добавления используйте setElement().delegateEvents()для установки дочернего элемента к элементу в родительском шаблоне.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}

Проблемы:

  • Это делает delegateEvents()необходимое сейчас, что является небольшим недостатком по сравнению с необходимостью только при последующих вызовах в первом сценарии.

Сценарий третий:

render()Вместо этого инициализируйте дочерние элементы в методе родителя .

initialize : function () {

    //parent init stuff
},

render : function () {

    this.$el.html(this.template());

    this.child = new Child();

    this.child.appendTo($.('.container-placeholder').render();
}

Проблемы:

  • Это означает, что теперь функция рендеринга должна быть связана со всей логикой инициализации.

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


Очень любопытно, чтобы ваши парни взяли это на себя. Какой сценарий вы бы использовали? или есть четвертый магический, который решает все эти проблемы?

Вы когда-нибудь отслеживали визуализированное состояние для View? Скажи renderedBeforeфлаг? Кажется, действительно вяло.

Йен Сторм Тейлор
источник
1
Я обычно не делаю ссылки на дочерние представления родительских представлений, потому что большая часть общения происходит через модели / коллекции и события, вызванные их изменениями. Хотя третий случай наиболее близок к тому, что я использую большую часть времени. Случай 1, где это имеет смысл. Кроме того, большую часть времени вам следует перерисовывать не весь вид, а скорее ту часть, которая изменилась
Том Ту
Конечно, я определенно рендерил только измененные части, когда это возможно, но даже тогда я думаю, что функция рендеринга должна быть доступной и неразрушающей. Как вы справляетесь с отсутствием ссылки на детей в родительском?
Ян Сторм Тейлор
Обычно я слушаю события в моделях, связанных с дочерними представлениями - если я хочу сделать что-то более нестандартное, я привязываю события к подпредставлениям. Если требуется такая «коммуникация», я обычно создаю вспомогательный метод для создания подпредставлений, которые связывают события и т. Д.
Том Ту,
1
Связанное обсуждение более высокого уровня см .: stackoverflow.com/questions/10077185/… .
Бен Робертс
2
Во втором сценарии зачем звонить delegateEvents()после setElement()? Согласно документации: «... и переместить делегированные события представления из старого элемента в новый», сам setElementметод должен обрабатывать повторное делегирование событий.
рдамборский

Ответы:

260

Это большой вопрос. Магистраль великолепна из-за отсутствия допущений, которые она делает, но это означает, что вы должны (решить, как) реализовать подобные вещи самостоятельно. Просматривая свои собственные материалы, я обнаружил, что я (вроде) использую сочетание сценария 1 и сценария 2. Я не думаю, что существует четвертый магический сценарий, потому что достаточно просто все, что вы делаете в сценарии 1 и 2, должно быть сделано.

Я думаю, что было бы проще объяснить на примере, как мне нравится справляться с этим. Скажем, у меня есть эта простая страница, разбитая на указанные виды:

Разбивка страницы

Скажем, HTML после рендеринга выглядит примерно так:

<div id="parent">
    <div id="name">Person: Kevin Peel</div>
    <div id="info">
        First name: <span class="first_name">Kevin</span><br />
        Last name: <span class="last_name">Peel</span><br />
    </div>
    <div>Phone Numbers:</div>
    <div id="phone_numbers">
        <div>#1: 123-456-7890</div>
        <div>#2: 456-789-0123</div>
    </div>
</div>

Надеюсь, это довольно очевидно, как HTML совпадает с диаграммой.

ParentViewИмеет 2 дочерние представления, InfoViewи PhoneListView, а также несколько дополнительных дивы, один из которых, #name, должен быть установлен в какой - то момент. PhoneListViewсодержит собственные дочерние представления, массив PhoneViewзаписей.

Итак, к вашему актуальному вопросу. Я занимаюсь инициализацией и рендерингом по-разному в зависимости от типа представления. Я делю свои взгляды на два типа: Parentвзгляды и Childвзгляды.

Разница между ними проста: Parentпредставления содержат дочерние представления, а Childпредставления - нет. Так что в моем примере, ParentViewи PhoneListViewявляются Parentмнением, в то время как InfoViewи PhoneViewзаписи являются Childвзглядами.

Как я уже упоминал ранее, самая большая разница между этими двумя категориями заключается в том, что им разрешено отображать. В идеальном мире я хочу, чтобы Parentпредставления отображались только один раз. Их дочерние представления должны обрабатывать любой повторный рендеринг при изменении модели. Childвиды, с другой стороны, я разрешаю перерисовывать в любое время, когда им нужно, поскольку у них нет других представлений, полагающихся на них.

Чуть более подробно, для Parentпредставлений мне нравится, что мои initializeфункции выполняют несколько вещей:

  1. Инициализировать мой собственный взгляд
  2. Предоставить мой собственный взгляд
  3. Создайте и инициализируйте любые дочерние представления.
  4. Назначьте каждому дочернему виду элемент в моем представлении (например InfoView, будет назначен #info).

Шаг 1 довольно понятен.

Шаг 2, рендеринг, сделан таким образом, что любые элементы, на которые полагаются дочерние представления, уже существуют, прежде чем я попытаюсь назначить их. Делая это, я знаю, что все дочерние eventsэлементы будут правильно установлены, и я могу перерисовывать их блоки столько раз, сколько захочу, не беспокоясь о необходимости повторного делегирования. На самом деле я не вижу renderздесь никаких детских взглядов, я позволяю им делать это в рамках их собственных initialization.

Шаги 3 и 4 фактически обрабатываются в то же время, что и я, elпри создании дочернего представления. Мне нравится передавать элемент здесь, так как я считаю, что родитель должен определить, где, по его собственному мнению, ребенку разрешено размещать его содержимое.

Для рендеринга я стараюсь сделать его довольно простым для Parentпредставлений. Я хочу, чтобы renderфункция не делала ничего, кроме визуализации родительского представления. Без делегирования событий, без представления дочерних представлений, ничего. Просто простой рендер.

Иногда это не всегда работает, хотя. Например, в моем примере выше, #nameэлемент должен будет обновляться каждый раз, когда меняется имя в модели. Тем не менее, этот блок является частью ParentViewшаблона и не обрабатывается специальным Childпредставлением, поэтому я работаю над этим. Я создам какую-то subRenderфункцию, которая только заменяет содержимое #nameэлемента и не должна уничтожать весь #parentэлемент. Это может показаться хаком, но я действительно обнаружил, что это работает лучше, чем беспокоиться о повторном рендеринге всего DOM и повторного присоединения элементов и тому подобного. Если бы я действительно хотел сделать его чистым, я бы создал новое Childпредставление (похожее на InfoView), которое будет обрабатывать #nameблок.

Теперь для Childпредставлений initializationэто очень похоже на Parentпредставления, только без создания каких-либо дальнейших Childпредставлений. Так:

  1. Инициализировать мой взгляд
  2. Настройка связывает прослушивание любых изменений в модели, которая мне нужна
  3. Отдай мой взгляд

ChildПросмотр рендеринга также очень прост, просто отрендерите и установите содержимое моего el. Опять же, не возиться с делегацией или что-то в этом роде.

Вот пример кода того, как ParentViewможет выглядеть мой :

var ParentView = Backbone.View.extend({
    el: "#parent",
    initialize: function() {
        // Step 1, (init) I want to know anytime the name changes
        this.model.bind("change:first_name", this.subRender, this);
        this.model.bind("change:last_name", this.subRender, this);

        // Step 2, render my own view
        this.render();

        // Step 3/4, create the children and assign elements
        this.infoView = new InfoView({el: "#info", model: this.model});
        this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
    },
    render: function() {
        // Render my template
        this.$el.html(this.template());

        // Render the name
        this.subRender();
    },
    subRender: function() {
        // Set our name block and only our name block
        $("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
    }
});

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

Вот пример кода для InfoViewблока:

var InfoView = Backbone.View.extend({
    initialize: function() {
        // I want to re-render on changes
        this.model.bind("change", this.render, this);

        // Render
        this.render();
    },
    render: function() {
        // Just render my template
        this.$el.html(this.template());
    }
});

Связи являются важной частью здесь. Привязываясь к моей модели, мне никогда не придется беспокоиться о том, чтобы позвонить renderмне вручную . Если модель изменится, этот блок будет перерисован сам, не влияя на другие виды.

Это PhoneListViewбудет аналогично тому ParentView, что вам просто понадобится немного больше логики в ваших функциях initializationи renderфункциях для обработки коллекций. То, как вы обрабатываете коллекцию, действительно зависит от вас, но вам, по крайней мере, нужно будет слушать события коллекции и решать, как вы хотите визуализировать (добавить / удалить или просто заново визуализировать весь блок). Лично мне нравится добавлять новые виды и удалять старые, а не заново отображать весь вид.

PhoneViewБудет почти идентичен InfoView, только прислушиваясь к модели изменений , которые она заботится о.

Надеюсь, это немного помогло, пожалуйста, дайте мне знать, если что-то сбивает с толку или недостаточно подробно.

Кевин Пил
источник
8
Действительно подробное объяснение спасибо. Вопрос, однако, я слышал и склонен согласиться с тем, что вызов renderвнутри initializeметода - плохая практика, потому что он не позволяет вам быть более производительным в тех случаях, когда вы не хотите выполнять рендеринг сразу. Что Вы думаете об этом?
Ян Сторм Тейлор
Конечно. Хм ... Я только пытаюсь инициализировать представления, которые должны быть показаны прямо сейчас (или могут быть показаны в ближайшем будущем, например, форма редактирования, которая скрыта сейчас, но отображается при нажатии на ссылку редактирования), поэтому они должны отображаться немедленно. Если, скажем, у меня есть случай, когда представление занимает некоторое время для рендеринга, я лично не создаю само представление, пока оно не будет показано, поэтому инициализация и рендеринг не будут выполняться до тех пор, пока это не потребуется. Если у вас есть пример, я могу попытаться более подробно рассказать о том, как я справляюсь с этим ...
Кевин Пил
5
Это большое ограничение - не иметь возможности повторно визуализировать ParentView. У меня есть ситуация, когда у меня есть элемент управления вкладками, где каждая вкладка показывает разные ParentView. Я хотел бы просто повторно выполнить рендеринг существующего ParentView при повторном нажатии на вкладку, но это приводит к потере событий в ChildViews (я создаю дочерние элементы в init и отображаю их в render). Я предполагаю, что я либо 1) скрываю / показываю вместо рендера, 2) делегирую события в рендере ChildViews, либо 3) создаю дочерние элементы в рендере, либо 4) не беспокоюсь о perf. прежде чем это проблема, и заново создайте ParentView каждый раз. Хмммм ....
Пол Хенеке
Я должен сказать, что это ограничение в моем случае. Это решение будет работать нормально, если вы не попробуете перерисовать родителя :) У меня нет хорошего ответа, но у меня тот же вопрос, что и у OP. Все еще надеемся найти правильный путь «Магистрали». На данный момент, я думаю, что скрыть / показать и не перерисовывать - это путь для меня.
Пол Хоенеке
1
как передать коллекцию ребенку PhoneListView?
Три Нгуен
6

Я не уверен, что это прямо отвечает на ваш вопрос, но я думаю, что это актуально:

http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/

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

Дерик Бэйли
источник
6

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

Как и Дерик, я не совсем уверен, отвечает ли это прямо на ваш вопрос, но я думаю, что, по крайней мере, стоит упомянуть в этом контексте.

Также см .: Использование Eventbus в Магистрали

борец
источник
3

Кевин Пил дает отличный ответ - вот моя версия:

initialize : function () {

    //parent init stuff

    this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!

    this.child = new Child();
},
Нейт Барр
источник
2

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

Использовать роутер

По сути, вы позволяете функции маршрутизатора инициализировать родительское и дочернее представление. Таким образом, представление не знает друг друга, но маршрутизатор обрабатывает все это.

Передача одного и того же эль в оба представления

this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});

Оба имеют знания об одном и том же DOM, и вы можете заказать их в любое время.

Сан Трён-Нгуён
источник
2

То, что я делаю, дает каждому ребенку личность (который Backbone уже сделал это для вас: cid)

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

<tagName id='cid'></tagName>

чем вы можете использовать

Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element 
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();

указанный заполнитель не требуется, и Container генерирует только заполнитель, а не дочернюю структуру DOM. Cotainer и Children по-прежнему генерируют собственные элементы DOM и только один раз.

Вилла
источник
1

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

https://github.com/rotundasoftware/backbone.subviews

Подход, используемый этим плагином, заключается в создании и визуализации подпредставлений после первого отображения родительского представления. Затем при последующих визуализациях родительского представления $ .detach элементы подпредставления повторно визуализируют родительский элемент, затем вставляют элементы подпредставления в соответствующие места и повторно визуализируют их. Таким образом, объекты подпредставлений повторно используются при последующих рендерингах, и нет необходимости повторно делегировать события.

Обратите внимание, что случай представления коллекции (где каждая модель в коллекции представлена ​​одним подпредставлением) весьма отличается и, я думаю, заслуживает своего собственного обсуждения / решения. Лучшее общее решение, которое мне известно, - это CollectionView in Marionette .

РЕДАКТИРОВАТЬ: Для случая представления коллекции, вы также можете проверить эту более ориентированную на пользовательский интерфейс реализацию , если вам нужен выбор моделей на основе щелчков и / или перетаскивания для изменения порядка.

Храбрый Дейв
источник