BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos RenderTree do Blazor Explicado

RenderTree do Blazor Explicado

Pontos Principais

  • O Blazor é um novo framework single page application (SPA) da Microsoft. Ao contrário dos demais frameworks SPA, como Angular ou React, este conta com com o framework .NET em prol do JavaScript.

  • O DOM (Document Object Model) é uma interface independente de plataforma e linguagem que trata um documento XML ou HTML como uma estrutura de árvore. Ele permite que o conteúdo do documento seja acessado e atualizado dinamicamente por sistemas e scripts.

  • O Blazor usa uma camada de abstração entre o DOM e o código da aplicação, chamado RenderTree. É uma cópia leve do estado do DOM composta por classes C# padrão.

  • O RenderTree pode ser atualizado com mais eficiência que o DOM e reconcilia várias alterações em uma única atualização. Para maximizar a eficácia, o RenderTree usa um algoritmo de diferenciação para garantir que sua atualização seja feita apenas nos elementos necessários do DOM no navegador.

  • O processo de mapeamento do DOM em uma RenderTree pode ser controlado com a diretiva @key. O controle desse processo pode ser necessário em certos cenários que exigem que o contexto de diferentes elementos do DOM sejam mantidos quando ele é atualizado.

O Blazor é um novo framework de single page application (SPA) da Microsoft. Ao contrário de outros frameworks SPA, como Angular ou React, o Blazor conta com o framework .NET em favor do JavaScript. O Blazor suporta muitos dos mesmos recursos encontrados nesses frameworks, incluindo um modelo robusto de desenvolvimento de componentes. A base do JavaScript, especialmente ao sair do mundo do jQuery, é uma mudança no pensamento sobre como os componentes são atualizados no navegador. O modelo de componente do Blazor foi desenvolvido para fornecer eficiência e conta com uma poderosa camada de abstração para maximizar o desempenho e a facilidade do uso.

Abstrair o DOM (Document Object Model) parece intimidador e complexo, no entanto, com as aplicações da web ficando cada vez mais modernas, isso se tornou algo comum. O principal motivo é que a atualização do que foi renderizado no navegador é uma tarefa intensiva em termos computacionais e as abstrações do DOM são usadas para intermediar a aplicação e o navegador para reduzir a quantidade de novas renderizações da tela. Para entender realmente o impacto do RenderTree do Blazor sobre a aplicação, precisamos primeiro lembrar do básico.

Vamos começar com uma definição rápida do DOM. Um Document Object Model ou DOM é uma interface independente de plataforma e linguagem que trata um documento XML ou HTML como uma estrutura de árvore. Na estrutura do DOM, cada nó é um objeto que faz parte do documento. Isso significa que o DOM é uma estrutura de documento de árvore lógica.

… O DOM é uma interface independente de plataforma e linguagem que trata um documento XML ou HTML como uma estrutura de árvore.

Quando uma aplicação Web é carregada no navegador, um JavaScript DOM é criado. Essa árvore de objetos atua como a interface entre o JavaScript e o documento real no navegador. Quando criamos aplicações web dinâmicas ou single page applications (SPAs) com JavaScript, usamos o básico da API do DOM. Quando usamos o DOM para criar, atualizar e excluir elementos HTML, modificamos o CSS e outros atributos, isso é conhecido como manipulação do DOM. Além de manipulá-lo, também podemos usá-lo para criar e responder a eventos.

Abaixo, temos uma página web básica com dois elementos, um h1 e um p. Quando o documento é carregado pelo navegador, um DOM é criado representando os elementos do HTML. Na figura 1 está uma representação da aparência do DOM como nós de uma árvore.
 

<!DOCTYPE html>
<html>
<body>
   <h1>Hello World</h1>
   <p id="beta">This is a sample document.</p>
</body>
</html>

Figura 1: Um documento HTML é carregado como uma árvore de nós, cada objeto representa um elemento no DOM.

Usando o JavaScript, podemos atravessar o DOM explicitamente referenciando os objetos na árvore. Começando com o nó raiz do documento, nós podemos percorrer os objetos filhos até chegarmos a um objeto ou propriedade desejado. Por exemplo, podemos tirar o segundo filho da ramificação do corpo chamando document.body.children [1] e depois recuperar o valor innerText como uma propriedade.

document.body.children[1].innerText
"This is a sample document."

Uma maneira mais fácil de recuperar o mesmo elemento é usar uma função que procurará no DOM por uma consulta específica. Existem vários jeitos de consultar o DOM usando vários seletores. Por exemplo, podemos recuperar o elemento p pelo seu id beta usando a função getElementById.

document.getElementById("beta").innerText
"This is a sample document."

Ao longo da história da web, os frameworks facilitaram o trabalho com o DOM. O jQuery é um framework que possui uma extensa API construída ao redor da manipulação do DOM. No exemplo a seguir, iremos recuperar novamente o texto do elemento p. Usando o método $ do jQuery, nós podemos encontrar facilmente o elemento pelo atributo id e acessar o texto.

//jQuery
$("#beta").text()
"This is a sample document."

A vantagem do jQuery são os métodos simples que reduzem a quantidade de código necessária para encontrar e manipular objetos. No entanto, a grande desvantagem dessa abordagem é o manuseio ineficiente das atualizações devido à alteração direta dos elementos no DOM. Uma vez que a manipulação direta do DOM é uma tarefa custosa computacionalmente, ela deve ser realizada com um pouco de cautela.

Como a manipulação direta do DOM é uma tarefa custosa computacionalmente, ela deve ser realizada com um pouco de cautela.

É uma prática comum, na maioria das aplicações, executar várias operações que atualizam o DOM. Usando uma abordagem típica do JavaScript ou jQuery, podemos remover um nó da árvore e substituí-lo por um novo conteúdo. Quando os elementos são atualizados desta maneira, os elementos e seus filhos geralmente podem ser removidos e substituídos quando nenhuma alteração for necessária. No exemplo a seguir, vários elementos semelhantes são removidos usando um seletor coringa, n-elements. Os elementos são substituídos, mesmo que precisem somente de uma modificação. Como podemos ver na figura 2, muitos elementos são removidos e substituídos enquanto apenas duas atualizações são necessárias.

// 1 = initial state
$(“n-elements”).remove() // 2-3
$(“blue”).append(modifiedElement1) // 4
$(”green”).append(modifiedElement2) // 4
$(“orange”).append(modifiedElement3) // 4

Figura 2: 1) O estado inicial. 2) Os elementos semelhantes são selecionados para remoção. 3) Os elementos e seus filhos são removidos do DOM. 4) Todos os elementos são substituídos com apenas algumas alterações.

Em uma aplicação Blazor, não somos responsáveis por fazer as alterações no DOM. O Blazor usa uma camada de abstração entre o DOM e o código que escrevemos da aplicação. A abstração do DOM do Blazor é chamada RenderTree e é uma cópia mais leve do estado do DOM. O RenderTree pode ser atualizado com mais eficiência e reconcilia várias alterações em uma única atualização. Para maximizar a eficácia, o RenderTree usa um algoritmo de diferenciação para garantir que a atualização apenas dos elementos necessários no DOM.

Se fizermos várias atualizações de elementos em uma aplicação Blazor dentro do mesmo escopo de trabalho, o DOM receberá apenas as alteração produzida pela diferença final. Quando executamos o trabalho, uma nova cópia do RenderTree é criada a partir das alterações, por meio de códigos ou ligações de dados (data binding). Quando o componente está pronto para ser renderizado novamente, o estado atual é comparado ao novo estado e a diferença é produzida. Somente os valores da diferença são aplicados ao DOM durante a atualização.

Vamos analisar mais de perto como o RenderTree pode reduzir potencialmente as atualizações do DOM. Na figura 3, começamos com o estado inicial com três elementos que receberão atualizações, green, blue e orange.

Figura 3: O estado inicial do RenderTree (esquerda) e DOM (direita). Os elementos com os valores green, blue e orange serão afetados pelo código.

Na figura 4, podemos ver o trabalho sendo realizado em várias etapas no mesmo ciclo. Os itens são removidos e substituídos, tendo como resultado final, apenas a troca do valor de green e blue. Após a conclusão do ciclo de vida, as diferenças são reconciliadas.

Figura 4: 1) Nosso RenderTree atual. 2-4) Alguns elementos removidos, substituídos e atualizados. 5) O estado atual e o novo estado são comparados para encontrar a diferença.

Figura 5: A diferença do RenderTree é usada para atualizar apenas os elementos que foram alterados durante a operação.

Criando um RenderTree

Em uma aplicação Blazor, os componentes Razor (.razor) são realmente processados de maneira bastante diferente das tradicionais marcações Razor Pages ou Views, (.cshtml). O Razor no contexto do MVC ou do Razor Pages é um processo unidirecional onde é renderizado no lado do servidor como HTML. Um componente do Blazor adota uma abordagem diferente, sua marcação é usada para gerar uma classe C# que cria o RenderTree. Vamos dar uma olhada no processo para ver como o RenderTree é criado.

Quando um componente Razor é criado, um arquivo .razor é adicionado ao nosso projeto, o conteúdo é usado para gerar uma classe C#. A classe gerada herda da classe ComponentBase, que inclui o método BuildRenderTree no componente, como mostrado na Figura 6. O BuildRenderTree é um método que recebe um objeto RenderTreeBuilder e anexa o componente à árvore, convertendo nossa marcação em objetos RenderTree.

Figura 6: O diagrama de classes do ComponentBase com o método BuildRenderTree sublinhado.

Usando o exemplo do componente Counter incluído no modelo .NET, podemos ver como o código do componente se torna uma classe gerada. No componente Counter, existem itens significativos que podemos identificar no RenderTree resultante, incluindo:

  1. Diretiva de roteamento de página
  2. h1 como sendo um elemento HTML básico
  3. p, currentCount é uma mistura de conteúdo estático e campo vinculado a um dado
  4. Botão com um manipulador de eventos onclick chamado IncrementCount
  5. Bloco de código com código C#
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

O código no exemplo Counter é usado para gerar uma classe com o método BuildRenderTree detalhada que descreve os objetos na árvore. Se examinarmos a classe gerada, podemos ver como os itens significativos foram traduzidos em código C# puro:

  1. Diretiva de página se torna uma tag de atributo na classe
  1. O Counter é uma classe pública que herda o ComponentBase
  1. AddMarkupContent define o conteúdo HTML como o elemento h1
  2. Elementos mistos como p, currentCount se tornam nós separados na árvore, definidos por tipos de conteúdo específicos, OpenElement e AddContent
  3. O botão inclui objetos de atributo para CSS e o manipulador de eventos onclick
  4. O código dentro do bloco de código é avaliado como código C#
[Route("/counter")]
public class Counter : ComponentBase
{
    private int currentCount = 0;

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.AddMarkupContent(0, "<h1>Counter</h1>\r\n\r\n");
        builder.OpenElement(1, "p");
        builder.AddContent(2, "Current count: ");
        builder.AddContent(3, this.currentCount);
        builder.CloseElement();
        builder.AddMarkupContent(4, "\r\n\r\n");
        builder.OpenElement(5, "button");
        builder.AddAttribute(6, "class", "btn btn-primary");
        builder.AddAttribute<MouseEventArgs>(7, "onclick", 
		EventCallback.Factory.Create<MouseEventArgs>(this, 
		new Action(this, Counter.IncrementCount)));
        builder.AddContent(8, "Click me");
        builder.CloseElement();
    }

    private void IncrementCount()
    {
        this.currentCount++;
    }
}

Podemos ver como a marcação e o código se transformam em um pedaço de lógica bem estruturada. Cada parte do componente é representada no RenderTree para que possa ser comunicado com eficiência ao DOM.

Incluído em cada item na árvore de renderização, está um número de sequência, por exemplo: AddContent(num, value). Os números de sequência estão incluídos para auxiliar o algoritmo de diferenciação e aumentar a eficiência. Ter um número inteiro bruto fornece ao sistema um indicador imediato para determinar se uma alteração ocorreu avaliando a ordem, presença ou ausência destes números e também usa a sequência para fazer as alterações. Por exemplo, se compararmos uma sequência de objetos 1,2,3 com 1,3, pode-se determinar que 2 foi removido do DOM.

O RenderTree é um utilitário poderoso que faz a parte da abstração para nós sendo utilizado por ferramentas inteligentes. Como podemos ver nos exemplos anteriores, os componentes são apenas classes C# padrão. Essas classes podem ser criadas manualmente, usando a classe ComponentBase e posteriormente escrevendo o método RenderTreeBuilder na mão. Embora seja possível, não seria aconselhável, além de ser considerado uma má prática. Os RenderTree gravados manualmente podem ter problemas se o número de sequência não for um número linear estático. O algoritmo de diferenciação precisa de completa previsibilidade, ou o componente pode renderizar novamente de maneira desnecessária, anulando sua eficiência.

O RenderTree gravado manualmente podem ser um problema se o número de sequência não for um número linear estático.

Otimizando a renderização dos componentes

Quando trabalhamos com a lista de elementos ou componentes no Blazor, devemos considerar como a lista de itens irá se comportar e as intenções de como os componentes serão usados. Por fim, o algoritmo de diferenciação do Blazor deve decidir como os elementos ou componentes podem ser retidos e como os objetos RenderTree devem ser mapeados para eles. O algoritmo de diferenciação geralmente pode ser ignorado, mas há casos em que podemos querer controlar o processo.

  1. Uma lista renderizada (por exemplo, em um bloco @foreach) que contenha um identificador único.
  2. Uma lista com elementos filhos que podem mudar com dados inseridos, excluídos ou reordenados.
  3. Nos casos em que a nova renderização leva a diferenças de comportamento visíveis, como perda do foco do elemento.

O processo de mapeamento do RenderTree pode ser controlado com o atributo de diretiva @key. Ao adicionar um @key, instruímos o algoritmo de diferenciação a preservar elementos ou componentes relacionados ao valor da chave. Vejamos um exemplo onde o @key é necessário e atende aos critérios listados acima (regras 1-3).

Uma lista não ordenada ul é criada. Dentro de cada item li da lista um h1 exibe o Value da classe Color. Além disso, em cada item da lista há uma entrada que exibe um elemento do checkbox. Para simular o trabalho que poderíamos fazer em uma lista como: Classificação, inserção ou remoção de itens, um botão é adicionado para reverter a lista. O botão usa uma função em linha, items = items.Reverse() para reverter a matriz dos itens quando o mesmo for clicado.

<ul class="list-group">
    @foreach (var item in items)
    {
        <li class="list-group-item">
            <h1>@item.Value</h1>
            <input type="checkbox" />
        </li>
    }
</ul>

<button @onclick="_ => items = items.Reverse()">Reverse</button>

@code {
    // class Color {int Id, string Value}
    IEnumerable<Color> items = new Color[] {
       new Color {Id = 0, Value = "Green" },
       new Color {Id = 1, Value = "Blue" },
       new Color {Id = 2, Value = "Orange" },
       new Color {Id = 3, Value = "Purple" }
    }; 

}

Quando executamos a aplicação, a lista é renderizada com um checkbox em cada item. Se marcarmos o checkbox no item "Green" e invertermos a lista, o checkbox selecionado permanecerá no topo e agora estará ocupando o lugar do item "Purple". Isso ocorre porque o algoritmo de diferenciação atualizou apenas o texto em cada elemento h1. O estado inicial e o estado reverso são mostrados na Figura 7, observe que a posição do checkbox permanece inalterada.

Figura 7: Um erro de renderização é visível, pois o checkbox falha ao se mover quando a matriz é revertida e o DOM perde o contexto do relacionamento do elemento.

Podemos usar a diretiva @key para fornecer os dados adicionais para o RenderTree. O @key irá indicar como cada item da lista está relacionado aos seus filhos. Com essa informação extra, o algoritmo de diferenciação pode preservar a estrutura do elemento. No exemplo, iremos atribuir o Id do item ao @key e posteriormente executar o aplicação novamente.

@foreach (var item in items)
{
    <li @key="item.Id" class="list-group-item">
        <h1>@item.Value</h1>
        <input type="checkbox" />
    </li>
}

Com a diretiva @key aplicada, o RenderTree irá criar, mover ou excluir os itens da lista e os elementos filhos associados. Se marcarmos o checkbox no item "Green" e invertermos a lista, o checkbox selecionado também será movido porque o RenderTree está movendo o grupo inteiro de elementos do li dentro da lista, isso pode ser visto na Figura 8.

Figura 8: Usando o atributo key, os elementos mantêm o relacionamento e o checkbox permanece no container apropriado conforme o DOM é atualizado.

Neste exemplo, tivemos um cenário ideal que atendeu aos critérios para a necessidade do @key. Consertamos os erros visuais causados pela nova renderização da lista de itens. No entanto, nem sempre os casos de uso são tão extremos, por isso é importante considerar cuidadosamente e entender as implicações da aplicação do @key.

Quando o @key não é usado, o Blazor preserva as instâncias de elemento e componente filho o máximo possível. A vantagem de usar o @key é o controle sobre como as instâncias do modelo que são mapeadas para as instâncias de componente preservados, ao invés do algoritmo de diferenciação selecionar o mapeamento. O uso do @key vem com um pequeno custo de desempenho, no entanto, se os elementos forem preservados pelo RenderTree, isso poderá resultar em um benefício.

Conclusão

Enquanto o RenderTree é abstraído através da sintaxe Razor nos arquivos .razor, é importante entender como isso afeta a maneira como escrevemos o código. Como vimos no exemplo, é essencial entender o RenderTree e seu funcionamento ao escrever componentes que gerenciam uma hierarquia. O atributo @key é essencial ao trabalhar com coleções e hierarquia, para que o RenderTree possa ser otimizado e evitar erros de renderização.

Sobre o Autor

Ed Charbeneau é o autor do Free Blazor E-book; Blazor a Beginners Guide, Microsoft MVP e um palestrante internacional, escritor, influenciador online, um Developer Advocate for Progress e especialista em tudo o que é desenvolvimento web. Charbeneau gosta de se refrescar com novas tecnologias, debater sobre tecnologias futuras e admirar um ótimo design.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT