Wie gehe ich mit dem Initialisieren und Rendern von Unteransichten in Backbone.js um?


199

Ich habe drei verschiedene Möglichkeiten, eine Ansicht und ihre Unteransichten zu initialisieren und zu rendern, und jede hat unterschiedliche Probleme. Ich bin gespannt, ob es einen besseren Weg gibt, um alle Probleme zu lösen:


Szenario Eins:

Initialisieren Sie die untergeordneten Elemente in der Initialisierungsfunktion des Elternteils. Auf diese Weise bleibt nicht alles beim Rendern hängen, sodass das Rendern weniger blockiert wird.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Die Probleme:

  • Das größte Problem besteht darin, dass beim zweiten Aufruf von render auf dem übergeordneten Element alle untergeordneten Ereignisbindungen entfernt werden. (Dies liegt daran, wie jQuery $.html()funktioniert.) Dies könnte durch einen Anruf gemildert werden. this.child.delegateEvents().render().appendTo(this.$el);Im ersten und häufigsten Fall erledigen Sie jedoch unnötig mehr Arbeit.

  • Durch Anhängen der untergeordneten Elemente erzwingen Sie, dass die Renderfunktion die DOM-Struktur der Eltern kennt, damit Sie die gewünschte Reihenfolge erhalten. Das bedeutet, dass zum Ändern einer Vorlage möglicherweise die Renderfunktion einer Ansicht aktualisiert werden muss.


Szenario Zwei:

Initialisieren Sie die Kinder im initialize()Standbild des Elternteils , aber anstatt sie anzuhängen, verwenden Sie sie setElement().delegateEvents(), um das Kind auf ein Element in der Elternvorlage zu setzen.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Probleme:

  • Dies macht das delegateEvents()Jetzt notwendig, was ein wenig negativ ist, da es nur bei nachfolgenden Aufrufen im ersten Szenario notwendig ist.

Szenario drei:

Initialisieren Sie render()stattdessen die untergeordneten Elemente in der übergeordneten Methode.

initialize : function () {

    //parent init stuff
},

render : function () {

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

    this.child = new Child();

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

Probleme:

  • Dies bedeutet, dass die Renderfunktion nun auch mit der gesamten Initialisierungslogik verknüpft werden muss.

  • Wenn ich den Status einer der untergeordneten Ansichten bearbeite und dann Render für das übergeordnete Element aufrufe, wird ein vollständig neues untergeordnetes Element erstellt und der gesamte aktuelle Status geht verloren. Das scheint auch für Speicherlecks heikel zu werden.


Wirklich neugierig darauf, dass eure Jungs das angehen. Welches Szenario würden Sie verwenden? oder gibt es eine vierte magische, die all diese Probleme löst?

Haben Sie jemals einen gerenderten Status für eine Ansicht verfolgt? Eine renderedBeforeFlagge sagen ? Scheint wirklich nervös.


1
Normalerweise schreibe ich keine Verweise auf untergeordnete Ansichten in übergeordnete Ansichten, da der größte Teil der Kommunikation über Modelle / Sammlungen und Ereignisse erfolgt, die durch Änderungen an diesen ausgelöst werden. Obwohl der dritte Fall dem am nächsten kommt, was ich die meiste Zeit benutze. Fall 1, wo es Sinn macht. Außerdem sollten Sie die meiste Zeit nicht die gesamte Ansicht neu rendern, sondern den Teil, der sich geändert hat
Tom Tu

Sicher, ich rendere definitiv nur die geänderten Teile, wenn möglich, aber selbst dann denke ich, dass die Renderfunktion verfügbar und sowieso zerstörungsfrei sein sollte. Wie gehen Sie damit um, dass Sie keinen Bezug zu den Kindern im Elternteil haben?
Ian Storm Taylor

Normalerweise höre ich Ereignisse in den Modellen, die den untergeordneten Ansichten zugeordnet sind. Wenn ich etwas Benutzerdefinierteres tun möchte, binde ich Ereignisse an Unteransichten. Wenn diese Art der 'Kommunikation' erforderlich ist, erstelle ich normalerweise eine Hilfsmethode zum Erstellen von Unteransichten, die die Ereignisse usw. binden.
Tom Tu

1
Eine übergeordnete Diskussion finden Sie unter: stackoverflow.com/questions/10077185/… .
Ben Roberts

2
Warum muss in Szenario 2 delegateEvents()nach dem angerufen werden setElement()? Gemäß den folgenden Dokumenten: "... und die delegierten Ereignisse der Ansicht vom alten auf das neue Element verschieben" sollte die setElementMethode selbst die erneute Delegierung von Ereignissen behandeln.
Rdamborsky

Antworten:


260

Das ist eine gute Frage. Backbone ist großartig, weil es keine Annahmen macht, aber es bedeutet, dass Sie solche Dinge selbst implementieren müssen. Nachdem ich meine eigenen Sachen durchgesehen habe, stelle ich fest, dass ich (irgendwie) eine Mischung aus Szenario 1 und Szenario 2 verwende. Ich glaube nicht, dass ein viertes magisches Szenario existiert, weil einfach alles, was Sie in Szenario 1 und 2 tun, sein muss getan.

Ich denke, es wäre am einfachsten zu erklären, wie ich mit einem Beispiel umgehen möchte. Angenommen, ich habe diese einfache Seite in die angegebenen Ansichten unterteilt:

Seitenaufschlüsselung

Angenommen, der HTML-Code ist nach dem Rendern ungefähr so:

<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>

Hoffentlich ist es ziemlich offensichtlich, wie der HTML-Code mit dem Diagramm übereinstimmt.

Das ParentViewhält 2 Kind Ansichten, InfoViewund PhoneListViewsowie ein paar zusätzliche divs, von denen eines , #namemuss an einem bestimmten Punkt eingestellt werden. PhoneListViewEnthält eigene untergeordnete Ansichten, eine Reihe von PhoneViewEinträgen.

Also weiter zu Ihrer eigentlichen Frage. Ich gehe mit der Initialisierung und dem Rendern je nach Ansichtstyp unterschiedlich um. Ich teile meine Ansichten in zwei Typen ein, ParentAnsichten und ChildAnsichten.

Der Unterschied zwischen ihnen ist einfach: ParentAnsichten enthalten untergeordnete Ansichten, ChildAnsichten jedoch nicht. Also in meinem Beispiel ParentViewund PhoneListViewsind ParentAnsichten, während InfoViewund die PhoneViewEinträge ChildAnsichten sind.

Wie ich bereits erwähnt habe, besteht der größte Unterschied zwischen diesen beiden Kategorien darin, wann sie rendern dürfen. In einer perfekten Welt möchte ich, dass ParentAnsichten immer nur einmal gerendert werden. Es liegt an ihren untergeordneten Ansichten, das erneute Rendern durchzuführen, wenn sich die Modelle ändern. ChildAnsichten hingegen erlaube ich, sie jederzeit neu zu rendern, da sie keine anderen Ansichten haben, die sich auf sie stützen.

Für ParentAnsichten möchte ich, dass meine initializeFunktionen einige Dinge tun:

  1. Initialisieren Sie meine eigene Ansicht
  2. Machen Sie meine eigene Ansicht
  3. Erstellen und initialisieren Sie untergeordnete Ansichten.
  4. Weisen Sie jeder untergeordneten Ansicht ein Element in meiner Ansicht zu (z. B. das InfoViewwürde zugewiesen werden #info).

Schritt 1 ist ziemlich selbsterklärend.

Schritt 2, das Rendern, wird ausgeführt, sodass alle Elemente, auf die sich die untergeordneten Ansichten stützen, bereits vorhanden sind, bevor ich versuche, sie zuzuweisen. Auf diese Weise weiß ich, dass alle Kinder eventsrichtig eingestellt sind, und ich kann ihre Blöcke so oft neu rendern, wie ich möchte, ohne mir Sorgen machen zu müssen, dass etwas neu delegiert werden muss. Ich habe hier eigentlich renderkeine kindlichen Ansichten, ich erlaube ihnen, das in ihren eigenen zu tun initialization.

Die Schritte 3 und 4 werden tatsächlich zur gleichen Zeit ausgeführt, zu der ich elbeim Erstellen der untergeordneten Ansicht übergebe. Ich möchte hier ein Element übergeben, da ich der Meinung bin, dass die Eltern bestimmen sollten, wo das Kind nach seiner eigenen Ansicht seinen Inhalt ablegen darf.

Beim Rendern versuche ich, es für ParentAnsichten ziemlich einfach zu halten . Ich möchte, dass die renderFunktion nur die übergeordnete Ansicht rendert. Keine Ereignisdelegation, kein Rendern von untergeordneten Ansichten, nichts. Nur ein einfaches Rendern.

Manchmal funktioniert das aber nicht immer. In meinem obigen Beispiel muss das #nameElement beispielsweise jedes Mal aktualisiert werden, wenn sich der Name im Modell ändert. Dieser Block ist jedoch Teil der ParentViewVorlage und wird nicht von einer dedizierten ChildAnsicht verarbeitet. Deshalb arbeite ich daran. Ich werde eine Art subRenderFunktion erstellen , die nur den Inhalt des #nameElements ersetzt und nicht das gesamte #parentElement in den Papierkorb werfen muss. Dies mag wie ein Hack erscheinen, aber ich habe wirklich festgestellt, dass es besser funktioniert, als sich Gedanken über das erneute Rendern des gesamten DOM und das erneute Anbringen von Elementen und dergleichen machen zu müssen. Wenn ich es wirklich sauber machen wollte, würde ich eine neue ChildAnsicht (ähnlich der InfoView) erstellen , die den #nameBlock handhaben würde .

Bei ChildAnsichten ist die Ansicht initializationziemlich ähnlich Parent, nur ohne dass weitere ChildAnsichten erstellt werden müssen. So:

  1. Initialisieren Sie meine Ansicht
  2. Setup bindet das Abhören von Änderungen an dem Modell, das mir wichtig ist
  3. Machen Sie meine Ansicht

ChildDas Rendern von Ansichten ist ebenfalls sehr einfach. Rendern Sie einfach den Inhalt von my und legen Sie ihn fest el. Wieder kein Durcheinander mit der Delegation oder so etwas.

Hier ist ein Beispielcode, wie mein ParentViewaussehen könnte:

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

Sie können meine Implementierung subRenderhier sehen. Dadurch , dass Änderungen gebunden subRenderstatt render, habe ich nicht zu befürchten Strahlen weg und den Wiederaufbau des gesamten Blocks.

Hier ist ein Beispielcode für den InfoViewBlock:

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

Die Bindungen sind hier der wichtige Teil. Durch die Bindung an mein Modell muss ich mich nie mehr darum kümmern, rendermich selbst manuell anzurufen. Wenn sich das Modell ändert, wird dieser Block selbst neu gerendert, ohne andere Ansichten zu beeinflussen.

Das PhoneListViewwird dem ähnlich sein ParentView, Sie benötigen nur ein wenig mehr Logik sowohl in Ihrer initializationals auch in Ihren renderFunktionen, um Sammlungen zu verwalten. Wie Sie mit der Sammlung umgehen, liegt ganz bei Ihnen, aber Sie müssen zumindest die Sammlungsereignisse abhören und entscheiden, wie Sie rendern möchten (Anhängen / Entfernen oder einfach das Rendern des gesamten Blocks). Ich persönlich möchte neue Ansichten anhängen und alte entfernen, nicht die gesamte Ansicht neu rendern.

Das PhoneViewwird fast identisch mit dem sein InfoView, nur das Hören der Modelländerungen, die es interessiert.

Hoffentlich hat dies ein wenig geholfen. Bitte lassen Sie mich wissen, wenn etwas verwirrend oder nicht detailliert genug ist.


8
Wirklich gründliche Erklärung danke. Frage, ich habe gehört und stimme eher zu, dass das Aufrufen renderinnerhalb der initializeMethode eine schlechte Praxis ist, da es Sie daran hindert, in Fällen, in denen Sie nicht sofort rendern möchten, leistungsfähiger zu sein. Was denkst du darüber?
Ian Storm Taylor

Sicher. Hmm ... Ich versuche nur, Ansichten zu initialisieren, die jetzt angezeigt werden sollen (oder in naher Zukunft angezeigt werden könnten, z. B. ein Bearbeitungsformular, das jetzt ausgeblendet ist, aber beim Klicken auf einen Bearbeitungslink angezeigt wird), daher sollten sie sofort gerendert werden. Wenn ich beispielsweise einen Fall habe, in dem das Rendern einer Ansicht eine Weile dauert, erstelle ich die Ansicht persönlich erst, wenn sie angezeigt werden muss, sodass erst bei Bedarf eine Initialisierung und ein Rendern stattfinden. Wenn Sie ein Beispiel haben, kann ich versuchen, detaillierter darauf einzugehen, wie ich damit umgehe ...
Kevin Peel

5
Es ist eine große Einschränkung, eine ParentView nicht erneut rendern zu können. Ich habe eine Situation, in der ich im Grunde ein Registerkartensteuerelement habe, bei dem jede Registerkarte eine andere Elternansicht anzeigt. Ich möchte die vorhandene ParentView nur erneut rendern, wenn ein zweites Mal auf eine Registerkarte geklickt wird. Dadurch gehen jedoch Ereignisse in den ChildViews verloren (ich erstelle untergeordnete Elemente in init und rendere sie in render). Ich denke, ich entweder 1) verstecke / zeige statt zu rendern, 2) delegiere Ereignisse im Rendering der ChildViews oder 3) erstelle Kinder im Rendern oder 4) mache mir keine Sorgen um die Perfektion. bevor es ein Problem ist und erstellen Sie die ParentView jedes Mal neu. Hmmmm ....
Paul Hoenecke

Ich sollte sagen, dass es in meinem Fall eine Einschränkung ist. Diese Lösung funktioniert einwandfrei, wenn Sie nicht versuchen, das übergeordnete Element erneut zu rendern :) Ich habe keine gute Antwort, aber ich habe die gleiche Frage wie OP. Ich hoffe immer noch, den richtigen 'Backbone'-Weg zu finden. An diesem Punkt denke ich, dass Verstecken / Zeigen und Nicht-Rendern der richtige Weg für mich ist.
Paul Hoenecke

1
Wie geben Sie die Sammlung an das Kind weiter PhoneListView?
Tri Nguyen


6

Für mich scheint es nicht die schlechteste Idee der Welt zu sein, zwischen dem Intital-Setup und den nachfolgenden Setups Ihrer Ansichten über eine Art Flag zu unterscheiden. Um dies sauber und einfach zu machen, sollte das Flag zu Ihrer eigenen Ansicht hinzugefügt werden, wodurch die Backbone-Ansicht (Basisansicht) erweitert werden soll.

Genau wie Derick bin ich mir nicht ganz sicher, ob dies Ihre Frage direkt beantwortet, aber ich denke, dass es in diesem Zusammenhang zumindest erwähnenswert sein könnte.

Siehe auch: Verwendung eines Eventbus im Backbone


3

Kevin Peel gibt eine großartige Antwort - hier ist meine tl; dr-Version:

initialize : function () {

    //parent init stuff

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

    this.child = new Child();
},

2

Ich versuche zu vermeiden, dass solche Ansichten gekoppelt werden. Normalerweise mache ich zwei Möglichkeiten:

Verwenden Sie einen Router

Grundsätzlich lassen Sie Ihre Router-Funktion die übergeordnete und untergeordnete Ansicht initialisieren. Die Ansicht kennt sich also nicht, aber der Router kümmert sich um alles.

Übergabe des gleichen El an beide Ansichten

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

Beide kennen dasselbe DOM und können es beliebig bestellen.


2

Ich gebe jedem Kind eine Identität (was Backbone bereits für Sie getan hat: cid)

Wenn Container das Rendern durchführt, wird mithilfe von 'cid' und 'tagName' ein Platzhalter für jedes Kind generiert, sodass die Kinder in der Vorlage keine Ahnung haben, wo es vom Container abgelegt wird.

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

als Sie verwenden können

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

Es wird kein angegebener Platzhalter benötigt, und Container generiert nur den Platzhalter und nicht die DOM-Struktur der Kinder. Cotainer und Children generieren immer noch und nur einmal eigene DOM-Elemente.


1

Hier ist ein leichtes Mixin zum Erstellen und Rendern von Unteransichten, das meiner Meinung nach alle Probleme in diesem Thread behebt:

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

Der Ansatz dieses Plugins besteht darin, Unteransichten nach dem ersten Rendern der übergeordneten Ansicht zu erstellen und zu rendern . Beim späteren Rendern der übergeordneten Ansicht $ .trennen Sie die Unteransichtselemente, rendern Sie das übergeordnete Element erneut, fügen Sie die Unteransichtselemente an den entsprechenden Stellen ein und rendern Sie sie erneut. Auf diese Weise werden Unteransichten von Objekten beim nachfolgenden Rendern wiederverwendet, und Ereignisse müssen nicht erneut delegiert werden.

Beachten Sie, dass der Fall einer Sammlungsansicht (bei der jedes Modell in der Sammlung mit einer Unteransicht dargestellt wird) ganz anders ist und meiner Meinung nach eine eigene Diskussion / Lösung verdient. Die beste allgemeine Lösung, die mir für diesen Fall bekannt ist, ist die CollectionView in Marionette .

BEARBEITEN: Für den Fall der Sammlungsansicht möchten Sie möglicherweise auch diese stärker auf die Benutzeroberfläche ausgerichtete Implementierung überprüfen , wenn Sie eine Auswahl von Modellen basierend auf Klicks und / oder Ziehen und Ablegen zur Neuordnung benötigen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.