Jak obsługiwać inicjowanie i renderowanie widoków podrzędnych w Backbone.js?

199

Mam trzy różne sposoby inicjowania i renderowania widoku i jego widoków podrzędnych, a każdy z nich ma inne problemy. Jestem ciekawy, czy istnieje lepszy sposób na rozwiązanie wszystkich problemów:


Scenariusz pierwszy:

Zainicjuj dzieci w funkcji inicjującej rodzica. W ten sposób nie wszystko utknie w renderowaniu, więc blokowanie renderowania jest mniejsze.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Problemy:

  • Największym problemem jest to, że wywołanie renderowania na obiekcie nadrzędnym po raz drugi usunie wszystkie powiązania zdarzeń potomnych. (Wynika to z tego, jak $.html()działa jQuery .) Można to złagodzić, wywołując this.child.delegateEvents().render().appendTo(this.$el);zamiast tego, ale w pierwszym, a najczęściej w przypadku, niepotrzebnie wykonujesz więcej pracy.

  • Dołączając dzieci, zmuszasz funkcję renderowania do znajomości struktury DOM rodziców, aby uzyskać pożądaną kolejność. Co oznacza, że ​​zmiana szablonu może wymagać aktualizacji funkcji renderowania widoku.


Scenariusz drugi:

Zainicjuj dzieci w kadrze nadrzędnym initialize(), ale zamiast dołączania użyj, setElement().delegateEvents()aby ustawić element podrzędny w szablonie nadrzędnym .

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Problemy:

  • To sprawia, że delegateEvents()konieczne jest teraz, co jest nieznacznie ujemne, ponieważ jest konieczne tylko w przypadku kolejnych połączeń w pierwszym scenariuszu.

Scenariusz trzeci:

render()Zamiast tego zainicjuj dzieci w metodzie rodzica .

initialize : function () {

    //parent init stuff
},

render : function () {

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

    this.child = new Child();

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

Problemy:

  • Oznacza to, że funkcja renderowania musi być teraz powiązana z całą logiką inicjalizacji.

  • Jeśli edytuję stan jednego z widoków podrzędnych, a następnie wywołam render na obiekcie nadrzędnym, zostanie utworzone zupełnie nowe dziecko, a cały jego bieżący stan zostanie utracony. Co również wydaje się, że może stać się groźny z powodu wycieków pamięci.


Naprawdę ciekawi mnie, jak chłopaki się tym zajmują. Którego scenariusza byś użył? czy jest czwarty magiczny, który rozwiązuje wszystkie te problemy?

Czy kiedykolwiek śledziłeś stan renderowanego widoku? Powiedz renderedBeforeflagę? Wydaje się naprawdę szalony.

Ian Storm Taylor
źródło
1
zazwyczaj nie mam na myśli odniesień do widoków potomnych w widokach rodzicielskich, ponieważ większość komunikacji odbywa się za pośrednictwem modeli / kolekcji i zdarzeń wywołanych przez zmiany w nich. Chociaż trzeci przypadek jest najbliższy temu, z którego korzystam przez większość czasu. Przypadek 1, w którym ma to sens. Również przez większość czasu nie powinieneś renderować całego widoku, a raczej część, która się zmieniła
Tom Tu
Jasne, zdecydowanie renderuję tylko zmienione części, jeśli to możliwe, ale nawet wtedy uważam, że funkcja renderowania powinna być dostępna i nieniszcząca. Jak radzisz sobie z brakiem odniesienia do dzieci w rodzicu?
Ian Storm Taylor
Zwykle słucham zdarzeń na modelach powiązanych z widokami potomnymi - jeśli chcę zrobić coś niestandardowego, wiążę zdarzenia z widokami podrzędnymi. Jeśli ten rodzaj „komunikacji” jest wymagany, zwykle tworzę metodę pomocniczą do tworzenia widoków podrzędnych, które wiążą zdarzenia itp.
Tom Tu
1
Na wyższym poziomie, związane dyskusja, patrz: stackoverflow.com/questions/10077185/... .
Ben Roberts,
2
W scenariuszu drugim, dlaczego trzeba dzwonić delegateEvents()po setElement()? Zgodnie z dokumentami: „... i przenieś delegowane zdarzenia widoku ze starego elementu na nowy”, setElementsama metoda powinna obsłużyć delegację zdarzeń.
rdamborsky

Odpowiedzi:

260

To świetne pytanie. Szkielet jest świetny ze względu na brak założeń, które sprawia, ale oznacza to, że musisz (zdecydować, jak) wdrożyć takie rzeczy samodzielnie. Po przejrzeniu moich własnych rzeczy stwierdzam, że (w pewnym sensie) używam kombinacji scenariusza 1 i scenariusza 2. Nie sądzę, aby istniał czwarty magiczny scenariusz, ponieważ po prostu wszystko, co robisz w scenariuszu 1 i 2, musi być Gotowe.

Myślę, że najłatwiej byłoby wyjaśnić na przykład, jak lubię sobie z tym poradzić. Powiedzmy, że mam tę prostą stronę podzieloną na określone widoki:

Podział strony

Powiedzmy, że HTML po renderowaniu wygląda mniej więcej tak:

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

Mamy nadzieję, że to dość oczywiste, jak HTML pasuje do diagramu.

ParentViewPosiada 2 widoki dziecko, InfoViewi PhoneListViewjak również kilka dodatkowych div, z których jeden, #name, musi być ustawiony w pewnym momencie. PhoneListViewposiada własne widoki potomne, tablicę PhoneViewwpisów.

Tak więc na twoje aktualne pytanie. Inicjowanie i renderowanie wykonuję inaczej w zależności od typu widoku. Podzielam swoje poglądy na dwa typy: Parentwidoki i Childwidoki.

Różnica między nimi jest prosta, Parentwidoki zawierają widoki potomne, a Childwidoki nie. Więc w moim przykładzie, ParentViewi PhoneListViewParentwidoki, podczas gdy InfoViewa PhoneViewwpisy są Childwidoki.

Jak wspomniałem wcześniej, największą różnicą między tymi dwiema kategoriami jest to, kiedy można je renderować. W idealnym świecie chcę, aby Parentwidoki były renderowane tylko raz. Od ich widoków podrzędnych zależy, czy każde renderowanie zostanie zmienione po zmianie modelu (ów).ChildZ drugiej strony, zezwalam na ponowne renderowanie w dowolnym momencie, ponieważ nie mają na nich żadnych innych widoków.

Bardziej szczegółowo, dla Parentwidoków lubię moje initializefunkcje, aby zrobić kilka rzeczy:

  1. Zainicjuj mój własny widok
  2. Renderuj mój własny widok
  3. Utwórz i zainicjuj wszelkie widoki potomne.
  4. Przypisz każdemu widokowi podrzędnemu element w moim widoku (np. InfoViewZostanie przypisany #info).

Krok 1 jest dość oczywisty.

Krok 2, renderowanie, jest wykonywany, aby wszystkie elementy, na których polegają widoki potomne, już istniały, zanim spróbuję je przypisać. W ten sposób wiem, że wszystkie dzieci eventszostaną poprawnie ustawione i mogę ponownie renderować ich bloki tyle razy, ile chcę, nie martwiąc się o konieczność ponownej delegacji. W rzeczywistości nie widzę rendertutaj żadnych poglądów dzieci, pozwalam im to robić we własnym zakresie initialization.

Kroki 3 i 4 są w rzeczywistości obsługiwane w tym samym czasie, w którym przechodzę elpodczas tworzenia widoku potomnego. Lubię przekazywać tutaj element, ponieważ uważam, że rodzic powinien określić, gdzie jego zdaniem dziecko może umieścić swoją treść.

Do renderowania staram się, aby Parentwidoki były dość proste . Chcęrender funkcja nic więcej nie renderowała, niż renderowanie widoku nadrzędnego. Bez delegowania zdarzeń, bez renderowania widoków dzieci, nic. Po prostu prosty render.

Czasami jednak nie zawsze to działa. Na przykład w moim przykładzie powyżej #nameelement będzie wymagał aktualizacji za każdym razem, gdy zmieni się nazwa w modelu. Jednak ten blok jest częścią ParentViewszablonu i nie jest obsługiwany przez dedykowany Childwidok, więc omijam to. Stworzę coś w rodzaju subRenderfunkcji, która zastępuje tylko zawartość #nameelementu i nie musi niszczyć całego #parentelementu. To może wydawać się włamaniem, ale naprawdę przekonałem się, że działa lepiej niż martwić się o ponowne renderowanie całego DOM i ponowne podłączanie elementów i tym podobne. Gdybym naprawdę chciał to wyczyścić, stworzyłbym nowy Childwidok (podobny do InfoView), który obsługiwałby #nameblok.

Teraz dla Childwidoków initializationjest bardzo podobny do Parentwidoków, tylko bez tworzenia kolejnych Childwidoków. Więc:

  1. Zainicjuj mój widok
  2. Instalator wiąże nasłuchując wszelkich zmian w modelu, na którym mi zależy
  3. Renderuj mój widok

Childrenderowanie widoku jest również bardzo proste, wystarczy wyrenderować i ustawić zawartość mojego el. Znowu, nie zadzieraj z delegacją ani nic podobnego.

Oto przykładowy kod tego, jak ParentViewmoże wyglądać mój :

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

Tutaj możesz zobaczyć moją implementację subRender. Mając do czynienia ze zmianami subRenderzamiast render, nie muszę się martwić o odpalenie i odbudowanie całego bloku.

Oto przykładowy kod dla InfoViewbloku:

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

Wiązania są tutaj ważną częścią. Wiążąc się z moim modelem, nigdy nie muszę się martwić o ręczne wywołanie rendersiebie. Jeśli model się zmieni, ten blok zrenderuje się sam, bez wpływu na inne widoki.

PhoneListViewBędzie podobny do ParentView, to po prostu trzeba trochę więcej logiki w obu swoich initializationand renderfunkcje kolekcji klamek. Sposób obsługi kolekcji zależy od Ciebie, ale musisz przynajmniej wysłuchać wydarzeń związanych z kolekcją i zdecydować, jak chcesz renderować (dołączyć / usunąć lub po prostu zrenderować cały blok). Osobiście lubię dodawać nowe widoki i usuwać stare, a nie renderować cały widok.

PhoneViewBędzie prawie identyczny jak InfoViewtylko słuchać modelowych zmian to obchodzi.

Mam nadzieję, że to trochę pomogło, daj mi znać, jeśli coś jest mylące lub niewystarczająco szczegółowe.

Kevin Peel
źródło
8
Naprawdę dokładne wyjaśnienie dzięki. Pytanie jednak, słyszałem i zwykle zgadzam się, że wywoływanie renderw initializemetodzie jest złą praktyką, ponieważ uniemożliwia to zwiększenie wydajności w przypadkach, gdy nie chcesz od razu renderować. Co o tym myślisz?
Ian Storm Taylor
Pewnie. Hmm ... Próbuję tylko zainicjować widoki, które powinny być teraz wyświetlone (lub które mogą być pokazane w najbliższej przyszłości, np. Formularz edycji, który jest teraz ukryty, ale wyświetlany po kliknięciu linku edycji), więc powinny być renderowane natychmiast. Jeśli powiedzmy, że mam przypadek, w którym renderowanie zajmuje trochę czasu, osobiście nie tworzę samego widoku, dopóki nie będzie trzeba go wyświetlić, więc inicjalizacja i renderowanie nie będzie miało miejsca, dopóki nie będzie konieczne. Jeśli masz przykład, mogę spróbować bardziej szczegółowo opisać, jak sobie z tym poradzić ...
Kevin Peel
5
Dużym ograniczeniem jest niemożność ponownego renderowania ParentView. Mam sytuację, w której mam w zasadzie kontrolkę tabulatorów, gdzie każda karta pokazuje inny ParentView. Chciałbym po prostu ponownie renderować istniejący ParentView po kliknięciu karty ponownie, ale powoduje to utratę zdarzeń w ChildViews (tworzę dzieci w init i renderuję je w render). Myślę, że albo 1) ukryj / pokaż zamiast renderowania, 2) deleguj wydarzenia w renderowaniu ChildViews, lub 3) twórz dzieci w renderowaniu, lub 4) nie martw się o perf. zanim pojawi się problem i za każdym razem ponownie utwórz ParentView. Hmmmm ....
Paul Hoenecke,
Powinienem powiedzieć, że w moim przypadku jest to ograniczenie. To rozwiązanie będzie działać dobrze, jeśli nie spróbujesz ponownie renderować elementu nadrzędnego :) Nie mam dobrej odpowiedzi, ale mam to samo pytanie co OP. Wciąż mam nadzieję znaleźć właściwy sposób „kręgosłupa”. W tej chwili myślę, że ukrywanie / pokazywanie i ponowne renderowanie nie jest moją drogą.
Paul Hoenecke
1
jak przekazać kolekcję dziecku PhoneListView?
Tri Nguyen
6

Nie jestem pewien, czy to bezpośrednio odpowiada na twoje pytanie, ale myślę, że ma to znaczenie:

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

Kontekst, w którym utworzyłem ten artykuł, jest oczywiście inny, ale myślę, że dwa oferowane przeze mnie rozwiązania, a także zalety i wady każdego z nich, powinny skłonić cię do pójścia we właściwym kierunku.

Derick Bailey
źródło
6

Dla mnie nie wydaje się najgorszym pomysłem na świecie rozróżnienie między początkową konfiguracją a kolejnymi ustawieniami twoich widoków za pomocą jakiejś flagi. Aby było to czyste i łatwe, należy dodać flagę do własnego widoku, który powinien rozszerzyć widok szkieletu (podstawy).

Podobnie jak Derick, nie jestem całkowicie pewien, czy to bezpośrednio odpowiada na twoje pytanie, ale myślę, że warto w tym kontekście wspomnieć.

Zobacz także: Wykorzystanie Eventbus w sieci szkieletowej

Pięściarz
źródło
3

Kevin Peel daje świetną odpowiedź - oto moja wersja tl; dr:

initialize : function () {

    //parent init stuff

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

    this.child = new Child();
},
Nate Barr
źródło
2

Staram się unikać łączenia takich widoków. Zazwyczaj są dwa sposoby:

Użyj routera

Zasadniczo pozwalasz funkcji routera zainicjować widok nadrzędny i podrzędny. Tak więc widok nie zna się nawzajem, ale router obsługuje to wszystko.

Przekazywanie tego samego elementu do obu widoków

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

Oba mają wiedzę o tym samym DOM i możesz je zamówić w dowolnym momencie.

Sơn Trần-Nguyễn
źródło
2

To, co robię, to nadanie każdemu dziecku tożsamości (co Backbone już to dla ciebie zrobił: cid)

Gdy renderowanie wykonuje kontener, użycie „cid” i „tagName” generuje symbol zastępczy dla każdego dziecka, więc w szablonie dzieci nie mają pojęcia o tym, gdzie zostanie umieszczone przez kontener.

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

niż możesz użyć

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

nie jest potrzebny żaden określony symbol zastępczy, a kontener generuje tylko symbol zastępczy zamiast struktury DOM dzieci. Cotainer i Children wciąż generują własne elementy DOM i tylko raz.

Willa
źródło
1

Oto lekki miks do tworzenia i renderowania widoków podrzędnych, który moim zdaniem rozwiązuje wszystkie problemy w tym wątku:

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

Podejście zastosowane przez tę wtyczkę polega na tworzeniu i renderowaniu widoków podrzędnych po pierwszym renderowaniu widoku nadrzędnego. Następnie, podczas kolejnych renderowań widoku nadrzędnego, $ .dachuj elementy widoku podrzędnego, ponownie renderuj element nadrzędny, a następnie wstaw elementy podrzędne w odpowiednich miejscach i ponownie je wyrenderuj. W ten sposób widoki podrzędne są ponownie wykorzystywane w kolejnych rendererach i nie ma potrzeby ponownego delegowania zdarzeń.

Zauważ, że przypadek widoku kolekcji (gdzie każdy model w kolekcji jest reprezentowany przez jeden widok cząstkowy) jest zupełnie inny i, jak sądzę, zasługuje na własną dyskusję / rozwiązanie. Najlepszym ogólnym rozwiązaniem, jakie znam w tej sprawie, jest CollectionView in Marionette .

EDYCJA: W przypadku widoku kolekcji możesz również sprawdzić tę implementację bardziej skoncentrowaną na interfejsie użytkownika , jeśli potrzebujesz wyboru modeli na podstawie kliknięć i / lub przeciągania i upuszczania w celu zmiany kolejności.

Dzielny Dave
źródło