Como lidar com a inicialização e a renderização de subvisões no Backbone.js?


199

Eu tenho três maneiras diferentes de inicializar e renderizar uma exibição e suas subvisões, e cada uma delas tem problemas diferentes. Estou curioso para saber se existe uma maneira melhor de resolver todos os problemas:


Cenário Um:

Inicialize os filhos na função de inicialização dos pais. Dessa forma, nem tudo fica preso na renderização para que haja menos bloqueio na renderização.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Os problemas:

  • O maior problema é que a chamada de renderização no pai pela segunda vez removerá todas as ligações de eventos do filho. (Isso ocorre por causa do funcionamento do jQuery $.html().) Isso pode ser atenuado pela chamada this.child.delegateEvents().render().appendTo(this.$el);, mas no primeiro caso, e na maioria das vezes, você está fazendo mais trabalho desnecessariamente.

  • Ao anexar os filhos, você força a função de renderização a ter conhecimento da estrutura DOM dos pais para obter a ordem desejada. O que significa que a alteração de um modelo pode exigir a atualização da função de renderização de uma exibição.


Cenário Dois:

Inicialize os filhos no pai initialize()ainda, mas em vez de anexar, use setElement().delegateEvents()para definir o filho como um elemento no modelo dos pais.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Problemas:

  • Isso torna o delegateEvents()necessário agora, o que é um pouco negativo, pois só é necessário em chamadas subsequentes no primeiro cenário.

Cenário Três:

Inicialize os filhos no render()método dos pais .

initialize : function () {

    //parent init stuff
},

render : function () {

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

    this.child = new Child();

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

Problemas:

  • Isso significa que agora a função de renderização também deve ser associada a toda a lógica de inicialização.

  • Se eu editar o estado de uma das visualizações filho e, em seguida, chamar render no pai, um filho completamente novo será criado e todo o seu estado atual será perdido. O que também parece que pode ficar arriscado por vazamentos de memória.


Realmente curioso para entender o que os seus caras fazem. Qual cenário você usaria? ou existe um quarto mágico que resolve todos esses problemas?

Você já acompanhou um estado renderizado para uma View? Diga uma renderedBeforebandeira? Parece realmente irregular.


1
Em geral, não estou pesquisando referências às visualizações filho nas visualizações pai, porque a maior parte da comunicação ocorre através de modelos / coleções e eventos acionados por alterações nelas. Embora o terceiro caso seja o mais próximo do que estou usando na maioria das vezes. Caso 1, onde faz sentido. Além disso, na maioria das vezes, você não deve renderizar novamente a exibição inteira, mas a parte que mudou
Tom Tu

Certamente, eu definitivamente renderizo apenas as partes alteradas quando possível, mas mesmo assim acho que a função render deve estar disponível e não destrutiva. Como você lida com a não referência aos filhos nos pais?
Ian Storm Taylor

Normalmente, ouço eventos nos modelos associados às visualizações filho - se eu quiser fazer algo mais personalizado, vinculo eventos a subviews. Se este tipo de 'comunicação' é necessária, então eu estou normalmente criando método auxiliar para criar subviews que liga os eventos etc.
Tom Tu

1
Para uma discussão relacionada de nível superior, consulte: stackoverflow.com/questions/10077185/… .
21712 Ben Roberts

2
No cenário dois, por que é necessário ligar delegateEvents()após o setElement()? Conforme docs: "... e mova os eventos delegados da exibição do elemento antigo para o novo", o setElementpróprio método deve lidar com a re-delegação de eventos.
Rdamborsky 29/03

Respostas:


260

Esta é uma grande pergunta. O backbone é ótimo por causa da falta de suposições, mas significa que você precisa (decidir como) implementar coisas como essa. Depois de analisar minhas próprias coisas, acho que (meio que) uso uma mistura dos cenários 1 e 2. Não acho que exista um quarto cenário mágico porque, simplesmente, tudo o que você faz nos cenários 1 e 2 deve ser feito.

Eu acho que seria mais fácil explicar como eu gosto de lidar com isso com um exemplo. Digamos que esta página simples seja dividida nas visualizações especificadas:

Repartição da página

Digamos que o HTML seja, depois de renderizado, algo como isto:

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

Espero que seja bastante óbvio como o HTML corresponde ao diagrama.

Ele ParentViewpossui 2 visualizações filho InfoViewe PhoneListViewalgumas divs extras, uma das quais #nameprecisa ser definida em algum momento. PhoneListViewpossui visualizações filho próprias, uma matriz de PhoneViewentradas.

Então, vamos à sua pergunta real. Lido com a inicialização e a renderização de maneira diferente, com base no tipo de exibição. Eu divido minhas visões em dois tipos, Parentvisões e Childvisões.

A diferença entre eles é simples: as Parentvisualizações mantêm as visualizações filho, enquanto as Childvisualizações não. Então, no meu exemplo, ParentViewe PhoneListViewsão Parentvisualizações, enquanto InfoViewe as PhoneViewentradas são Childvisualizações.

Como mencionei antes, a maior diferença entre essas duas categorias é quando elas podem ser renderizadas. Em um mundo perfeito, quero que as Parentvisualizações sejam renderizadas apenas uma vez. Cabe às exibições filhas deles lidar com qualquer nova renderização quando os modelos forem alterados. Childvisualizações, por outro lado, permito reproduzir novamente a qualquer momento, pois elas não têm outras visualizações.

Em um pouco mais detalhadamente, para Parentvisualizações, gosto de minhas initializefunções para fazer algumas coisas:

  1. Inicializar minha própria visão
  2. Renderizar minha própria visão
  3. Crie e inicialize todas as visualizações filho.
  4. Atribua a cada filho filho um elemento da minha visão (por exemplo, o InfoViewseria atribuído #info).

O passo 1 é bastante auto-explicativo.

A etapa 2, a renderização, é feita para que todos os elementos que os filhos exibem já existam antes que eu tente atribuí-los. Ao fazer isso, eu conheço todas as criançasevents serão definidas corretamente e posso renderizar novamente seus blocos quantas vezes quiser, sem me preocupar em ter que delegar novamente nada. Na verdade, não tenho rendernenhuma visão infantil aqui, deixo que eles façam isso sozinhos initialization.

As etapas 3 e 4 são realmente tratadas ao mesmo tempo em que passo el ao criar a exibição filho. Gosto de passar um elemento aqui, pois acho que o pai deve determinar onde, em sua própria visão, o filho pode colocar seu conteúdo.

Para renderização, tento mantê-lo bastante simples para Parent visualizações. Quero que a renderfunção não faça nada além de renderizar a exibição pai. Nenhuma delegação de evento, nenhuma renderização de visualizações filho, nada. Apenas uma renderização simples.

Às vezes, isso nem sempre funciona. Por exemplo, no meu exemplo acima, o #nameelemento precisará ser atualizado sempre que o nome no modelo for alterado. No entanto, esse bloco faz parte do ParentViewmodelo e não é tratado por uma Childexibição dedicada , portanto, resolvo isso. Vou criar algum tipo de subRenderfunção que substitui apenas o conteúdo do #nameelemento e não precisa descartar o #parentelemento inteiro . Isso pode parecer um hack, mas eu realmente achei que funciona melhor do que ter que me preocupar em renderizar novamente todo o DOM e reconectar elementos. Se eu realmente quisesse torná-lo limpo, criaria uma nova Childexibição (semelhante à InfoView) que lidaria com o #namebloco.

Agora, para Childvisualizações, ela initializationé bastante semelhante às Parentvisualizações, apenas sem a criação de outras Childvisualizações. Assim:

  1. Inicializar minha visão
  2. A instalação vincula a escuta de quaisquer alterações no modelo de que me importo
  3. Renderizar minha visão

Childver renderização também é muito simples, basta renderizar e definir o conteúdo do meu el. Novamente, não mexa com delegação ou algo assim.

Aqui está um exemplo de código de como minha ParentViewaparência pode ser:

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

Você pode ver minha implementação subRenderaqui. Por ter alterações vinculadas a, e subRendernão render, não preciso me preocupar em explodir e reconstruir todo o bloco.

Aqui está um código de exemplo para o InfoViewbloco:

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

As ligações são a parte importante aqui. Ao vincular meu modelo, nunca preciso me preocupar em chamar manualmenterender me . Se o modelo for alterado, esse bloco será renderizado novamente sem afetar outras visualizações.

O PhoneListViewserá semelhante ao ParentView, você só precisa de um pouco mais lógica em ambos os seus initializatione renderfunções para coleções punho. A decisão de como você lida com a coleção é realmente sua, mas pelo menos você precisará ouvir os eventos da coleção e decidir como deseja renderizar (acrescentar / remover ou apenas renderizar novamente o bloco inteiro). Pessoalmente, gosto de acrescentar novas exibições e remover as antigas, e não renderizar novamente a exibição inteira.

O PhoneViewserá quase idêntico ao InfoView, apenas ouvindo as mudanças de modelo com as quais se preocupa.

Espero que isso tenha ajudado um pouco, por favor, deixe-me saber se algo é confuso ou não é detalhado o suficiente.


8
Explicação realmente minuciosa, obrigado. No entanto, já ouvi falar e tento concordar que chamar renderdentro do initializemétodo é uma prática ruim, porque impede que você seja mais eficiente nos casos em que não deseja render imediatamente. O que você pensa sobre isso?
Ian tempestade Taylor

Certo. Hmm ... Eu só tento inicializar visualizações que devem ser mostradas agora (ou podem ser exibidas em um futuro próximo, por exemplo, um formulário de edição que está oculto agora, mas exibido ao clicar em um link de edição), para que elas sejam renderizadas imediatamente. Se, digamos, eu tenho um caso em que uma exibição demora um pouco para renderizar, eu pessoalmente não a crio até que ela precise ser mostrada; portanto, nenhuma inicialização e renderização ocorrerão até que seja necessário. Se você tem um exemplo que posso tentar entrar em mais detalhes sobre como eu lidar com isso ...
Kevin Peel

5
É uma grande limitação não poder renderizar novamente um ParentView. Eu tenho uma situação em que basicamente tenho um controle de guia, em que cada guia mostra um ParentView diferente. Gostaria apenas de renderizar novamente o ParentView existente quando uma guia for clicada pela segunda vez, mas isso fará com que os eventos nos ChildViews sejam perdidos (eu crio filhos no init e os renderizo na renderização). Eu acho que 1) ocultar / mostrar em vez de renderizar, 2) delegarEventos na renderização dos ChildViews, ou 3) criar filhos em renderizar, ou 4) não se preocupar com o desempenho. antes que seja um problema e recrie o ParentView sempre. Hmmmm ....
Paul Hoenecke

Eu devo dizer que é uma limitação no meu caso. Esta solução funcionará bem se você não tentar renderizar novamente o pai :) Não tenho uma boa resposta, mas tenho a mesma pergunta que OP. Ainda na esperança de encontrar o caminho 'Backbone' adequado. Neste ponto, estou pensando em esconder / mostrar e não renderizar novamente é o caminho a percorrer para mim.
Paul Hoenecke

1
como você passa a coleção para a criança PhoneListView?
Tri Nguyen


6

Para mim, não parece a pior idéia do mundo diferenciar entre a configuração inicial e as configurações subseqüentes de suas visualizações por meio de algum tipo de sinalizador. Para tornar isso fácil e limpo, o sinalizador deve ser adicionado à sua própria exibição, que deve estender a exibição Backbone (Base).

Assim como Derick, não tenho certeza absoluta de que isso responda diretamente à sua pergunta, mas acho que talvez valha a pena mencionar nesse contexto.

Veja também: Uso de um Eventbus no Backbone


3

Kevin Peel dá uma ótima resposta - aqui está a minha versão tl; dr:

initialize : function () {

    //parent init stuff

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

    this.child = new Child();
},

2

Estou tentando evitar o acoplamento entre visualizações como essas. Normalmente, existem duas maneiras de fazer isso:

Use um roteador

Basicamente, você permite que a função do roteador inicialize a exibição pai e filho. Portanto, a exibição não tem conhecimento um do outro, mas o roteador lida com tudo.

Passando o mesmo el para as duas visualizações

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

Ambos têm conhecimento do mesmo DOM e você pode solicitá-los da maneira que desejar.


2

O que faço é dar a cada criança uma identidade (que o Backbone já fez por você: cid)

Quando o Container faz a renderização, o uso de 'cid' e 'tagName' gera um espaço reservado para cada filho, portanto, no modelo, os filhos não têm idéia de onde serão colocados pelo Container.

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

do que você pode usar

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

nenhum espaço reservado especificado é necessário e o Container gera apenas o espaço reservado, em vez da estrutura DOM dos filhos. Cotainer e Children ainda estão gerando elementos DOM próprios e apenas uma vez.


1

Aqui está uma combinação leve para criar e renderizar sub-visualizações, que eu acho que resolve todos os problemas deste segmento:

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

A abordagem adotada por este plug-in é criar e renderizar subvisões após a primeira vez que a visualização pai é renderizada. Em seguida, nas renderizações subsequentes da visualização pai, $ .detalhe os elementos da subvisão, renderize novamente o pai, insira os elementos da subvisão nos locais apropriados e os renderize novamente. Dessa maneira, os subviews objetos são reutilizados nas renderizações subseqüentes e não há necessidade de delegar novamente os eventos.

Observe que o caso de uma exibição de coleção (onde cada modelo da coleção é representado com uma subvisão) é bem diferente e merece sua própria discussão / solução, eu acho. A melhor solução geral que conheço nesse caso é o CollectionView in Marionette .

EDIT: Para o caso de exibição de coleção, você também pode conferir esta implementação mais focada na interface do usuário , se precisar de seleção de modelos com base em cliques e / ou arrastar e soltar para reordenar.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.