Olá Habr! Trago à sua atenção uma tradução do artigo "React Fiber Architecture", de Andrew Clark .
Entrada
O React Fiber é uma implementação progressiva do principal algoritmo React. Este é o culminar de um estudo de dois anos pela equipe de desenvolvimento do React.
O objetivo do Fiber é aumentar a produtividade ao desenvolver tarefas como animação, organizar elementos em uma página e mover elementos. Sua principal característica é a renderização incremental: a capacidade de dividir o trabalho de renderização em unidades e distribuí-las entre vários quadros.
Outros recursos importantes incluem a capacidade de pausar, cancelar ou reutilizar as atualizações recebidas da árvore DOM, a capacidade de priorizar diferentes tipos de atualizações e também a coordenação de primitivas.
Antes de ler este artigo, recomendamos que você se familiarize com os princípios básicos do React:
Revisão
O que é reconciliação?
Reconciliação é um algoritmo React usado para distinguir uma árvore de elementos da outra para determinar as partes que precisam ser substituídas.
Uma atualização é uma alteração nos dados usados para renderizar um aplicativo React. Isso geralmente é o resultado da chamada do método setState; O resultado final da renderização do componente.
A idéia principal da API do React é pensar nas atualizações como se elas pudessem levar a uma renderização completa do aplicativo. Isso permite que o desenvolvedor aja de maneira declarativa e não se preocupe com a racionalidade da transição do aplicativo de um estado para outro (de A para B, B para C, C para A etc.).
Em geral, a renderização de todo o aplicativo para cada alteração funciona apenas nos aplicativos mais tradicionais. No mundo real, isso afeta negativamente o desempenho. O ato inclui otimizações que criam uma visualização de renderização completa sem afetar uma grande parte do desempenho. A maioria dessas otimizações envolve um processo chamado reconciliação.
A reconciliação é um algoritmo por trás do que estamos acostumados a chamar de "DOM virtual". A definição é mais ou menos assim: quando você renderiza um aplicativo React, a árvore de elementos que descreve o aplicativo é gerada na memória reservada. Essa árvore é então incluída no ambiente de renderização - usando o exemplo de um aplicativo de navegador, é convertida em um conjunto de operações DOM. Quando o estado do aplicativo é atualizado (geralmente chamando setState), uma nova árvore é gerada. A nova árvore é comparada com a anterior para calcular e habilitar exatamente as operações necessárias para redesenhar o aplicativo atualizado.
Embora o Fiber seja uma implementação próxima do reconciliador, o algoritmo de alto nível explicado na documentação do React será praticamente o mesmo.
Conceitos chave:
- Diferentes tipos de componentes sugerem a geração de árvores substancialmente diferentes. O React não tentará compará-los, mas simplesmente substitua completamente a árvore antiga.
- As listas são distinguidas usando chaves. As chaves devem ser "persistentes, previsíveis e únicas".
Reconciliação vs. renderização
A árvore DOM é um dos ambientes que o React pode desenhar, o restante pode ser atribuído às visualizações nativas do iOS e Android usando o React Native (é por isso que o Virtual Dom é um nome pouco inapropriado).
A razão pela qual o React suporta tantos objetivos é porque o React é construído para que a reconciliação e a renderização sejam fases separadas. O reconciliador, trabalhando, calcula quais partes da árvore foram alteradas; o renderizador posteriormente usa essas informações para atualizar a árvore renderizada anteriormente.
Essa separação significa que o React DOM e o React Native podem usar seus próprios mecanismos de renderização ao usar a mesma ferramenta de cache, localizada no React Core.
A fibra é uma implementação reprojetada do algoritmo de reconciliação. Ele tem uma relação indireta com a renderização, enquanto os mecanismos de renderização (renderizações) podem ser alterados para suportar todas as vantagens da nova arquitetura.
O planejamento é um processo que determina quando o trabalho deve ser concluído.
Trabalho - todos os cálculos que devem ser realizados. O trabalho geralmente é o resultado de uma atualização (por exemplo, chamando setState).
Os princípios da arquitetura React são tão bons que só podem ser descritos com esta citação:
Na implementação atual do React, ele percorre a árvore recursivamente e chama as funções de renderização em toda a árvore atualizada em um único tique (16 ms). No entanto, no futuro, ele poderá cancelar algumas atualizações para evitar saltos de quadros.
Este é um tópico frequentemente discutido sobre o React Design. Algumas bibliotecas populares implementam uma abordagem "push", onde os cálculos são realizados quando novos dados estão disponíveis. No entanto, o React segue a abordagem pull, onde os cálculos podem ser cancelados quando necessário.
O React não é uma biblioteca para processamento de dados generalizados. Esta é uma biblioteca para criar interfaces de usuário. Pensamos que ele deveria ter uma posição única na aplicação para determinar quais cálculos são adequados e quais não são no momento.
Se houver algo nos bastidores, podemos desfazer toda a lógica associada a ele. Se os dados chegarem mais rápido que a taxa de renderização de quadros, podemos combinar as atualizações. Podemos aumentar a prioridade do trabalho resultante da interação do usuário (como a aparência de uma animação quando um botão é pressionado) contra trabalhos menos importantes em segundo plano (renderização de novo conteúdo carregado do servidor) para impedir downloads de quadros.
Conceitos chave:
- Nas interfaces do usuário, não é importante que cada atualização seja aplicada imediatamente; de fato, esse comportamento será supérfluo, contribuirá para a queda de quadros e a deterioração do UX.
- Diferentes tipos de atualizações têm prioridades diferentes - as atualizações de animação devem terminar mais rápido do que, por exemplo, atualizar o armazenamento de dados.
- Uma abordagem baseada em envio exige que o aplicativo (você, o desenvolvedor) decida como planejar o trabalho. Uma abordagem baseada em pull permite que a estrutura tome decisões por você.
Reagir no momento não tem a vantagem de planejar uma extensão significativa; os resultados da atualização para toda a subárvore serão desenhados imediatamente. Selecionar cuidadosamente os elementos no algoritmo React do kernel para aplicar o agendamento é a principal ideia do Fiber.
O que é fibra?
Discutiremos o coração da arquitetura React Fiber. A fibra é uma abstração de nível inferior sobre o aplicativo do que os desenvolvedores estão acostumados a pensar. Se você considerar suas tentativas de entendê-lo sem esperança, não se sinta desencorajado (você não está sozinho). Continue procurando e ele finalmente dará frutos.
E assim!
Atingimos esse objetivo principal da arquitetura Fiber - permitindo que o React aproveite o planejamento. Especificamente, precisamos ser capazes de:
- pare o trabalho e volte a ele mais tarde.
- priorizar diferentes tipos de trabalho.
- reutilize o trabalho feito anteriormente.
- cancele o trabalho se não for mais necessário.
Para fazer tudo isso, primeiro precisamos dividir o trabalho em unidades. Em certo sentido, isso é fibra. Fibra representa uma unidade de trabalho.
Para ir além, vamos voltar ao conceito básico de React "componentes como dados de função" , geralmente expressos como:
v = f(d)
Com isso, resulta que renderizar um aplicativo React é como chamar uma função cujo corpo contém chamadas para outras funções, e assim por diante. Essa analogia é útil quando se pensa em fibras.
A maneira como os computadores basicamente verificam a ordem de execução de um programa é chamada de pilha de chamadas. Quando a função é concluída, o novo contêiner de pilha é adicionado à pilha. Este contêiner de pilha representa o trabalho realizado por uma função.
Ao trabalhar com interfaces com o usuário, muito trabalho é feito imediatamente e isso é um problema, pode levar a saltos na animação e parecerá intermitente. Além disso, parte desse trabalho pode não ser necessária se for substituída pela atualização mais recente. Nesse ponto, a comparação entre a interface do usuário e a função diverge, porque os componentes têm uma responsabilidade mais específica do que as funções em geral.
Os navegadores mais recentes e o React Native implementam APIs que ajudam a resolver esse problema:
requestIdleCallback distribui tarefas para que funções com baixa prioridade sejam chamadas em um período simples e requestAnimationFrame distribui tarefas para que funções com alta prioridade sejam chamadas no próximo quadro. O problema é que, para usar essas APIs, é necessário dividir o trabalho de renderização em unidades incrementais. Se você confiar apenas na pilha de chamadas, o trabalho continuará até que a pilha esteja vazia.
Não seria bom se pudéssemos personalizar o comportamento da pilha de chamadas para otimizar a exibição de partes da interface do usuário? Seria bom se pudéssemos quebrar a pilha de chamadas para manipular contêineres manualmente?
Esse é o chamado da React Fiber. Fiber é uma nova implementação de pilha adaptada aos componentes React. Você pode pensar em uma única fibra como um contêiner de pilha virtual.
A vantagem dessa implementação da pilha é que você pode salvar a pilha de contêineres na memória e executar (e onde) você desejar. Esta é uma definição crucial para alcançar seus objetivos de planejamento.
Além do planejamento, as ações manuais com a pilha revelam o potencial de conceitos como consistência (simultaneidade) e tratamento de erros (limites de erro).
Na próxima seção, veremos a estrutura das fibras.
Estrutura de fibra
Especificamente, uma “fibra” é um objeto JavaScript que contém informações sobre um componente, sua entrada e saída.
A fibra é consistente com o recipiente da pilha, mas também é consistente com a essência do componente.
Aqui estão algumas propriedades importantes da "fibra" (esta lista não é completa):
Tipo e chave
O tipo e a chave servem à fibra, bem como aos elementos React. De fato, quando uma fibra é criada, esses dois campos são copiados diretamente para ela.
O tipo de fibra descreve o componente ao qual corresponde. Para composição de componentes, tipo é uma função ou classe de componente. Para componentes de serviço (div, span), o tipo é uma sequência.
Conceitualmente, um tipo é uma função cuja execução é rastreada por um contêiner de pilha.
Juntamente com o tipo, a chave é usada na comparação de árvores para determinar se a fibra pode ser reutilizada.
Criança e irmão
Esses campos apontam para outras fibras, descrevendo a estrutura recursiva das fibras.
O filho da fibra corresponde ao valor retornado como resultado da chamada do método de renderização no componente. No exemplo abaixo:
function Parent() { return <Child /> }
Filho de fibra pai corresponde a filho.
O campo relativo (ou vizinho) será usado se render retornar vários filhos (um novo recurso no Fiber):
function Parent() { return [<Child1 />, <Child2 />] }
As fibras filhas são uma lista vinculada unicamente no início da qual é o primeiro filho. Portanto, neste exemplo, o filho Parent é Child1 e os parentes de Child1 são Child2.
Voltando à nossa analogia com as funções, você pode pensar em uma fibra filho como uma função chamada no final (função chamada cauda).
Exemplo da Wikipedia:
function foo(data) { a(data); return b(data); }
Neste exemplo, a função chamada de cauda é b.
Valor de retorno (retorno)
A fibra de retorno é a fibra à qual o programa deve retornar após o processamento da fibra atual. É o mesmo que retornar o endereço do contêiner da pilha.
Também pode ser considerada uma fibra pai.
Se uma fibra tiver várias fibras filho, o retorno de cada fibra filho retornará a fibra pai. No exemplo acima, a fibra de retorno de Child1 e Child2 é Parent.
Propriedades atuais e armazenadas em cache (pendingProps e memorizedProps)
Conceitualmente, propriedades são argumentos de função. As propriedades atuais da fibra são um conjunto dessas propriedades no início da execução, as em cache são um conjunto no final da execução.
Quando as propriedades de espera de entrada são armazenadas em cache, isso significa que a saída de fibra anterior pode ser reutilizada sem nenhum cálculo.
Prioridade do trabalho atual (pendingWorkPriority)
A quantidade de trabalho determinante de prioridade é exibida pela fibra. O módulo de nível de prioridade no React ReactPrioritylevel inclui diferentes níveis de prioridade e o que eles representam.
Começando com uma exceção do tipo NoWork, que é 0, um número mais alto define a menor prioridade. Por exemplo, você pode usar a seguinte função para verificar se a prioridade da fibra é maior que o nível especificado:
function matchesPriority(fiber, priority) { return fiber.pendingWorkPriority !== 0 && fiber.pendingWorkPriority <= priority }
Esta função é apenas para fins ilustrativos; não faz parte do banco de dados React Fiber.
O planejador usa o campo de prioridade para encontrar a próxima unidade de trabalho que pode ser executada. Discutiremos esse algoritmo na próxima seção.
Alternativa (ou par)
Atualização de fibra (nivelada) - isso significa exibir sua saída na tela.
Fibra em desenvolvimento (trabalho em andamento) - fibra que ainda não foi construída; em outras palavras, é um contêiner de pilha que ainda não foi retornado.
A qualquer momento, a essência do componente não possui mais de dois estados para a fibra, o que corresponde a: fibra no estado atual, fibra atualizada ou fibra em desenvolvimento.
A fibra atual é seguida pela fibra que está sendo desenvolvida e, por sua vez, a fibra é atualizada.
O próximo estado da fibra é criado preguiçosamente usando a função cloneFiber. Quase sempre ao criar um novo objeto, o cloneFiber tentará reutilizar uma alternativa (par) de fibra, se houver, minimizando o custo dos recursos.
Você deve pensar no campo de vapor (ou alternativo) como um detalhe de implementação, mas ele aparece com tanta frequência na documentação que era simplesmente impossível não mencioná-lo.
Conclusão é um elemento de serviço (ou um conjunto de elementos de serviço); nós de folha Reagir aplicações. Eles são específicos para cada ambiente de exibição (por exemplo, em um navegador, é 'div', 'span' etc.). No JSX, eles são indicados como nomes de tags em minúsculas.
Conclusão: recomendo experimentar os recursos da nova arquitetura React v16.0