We want to hear from you!Take our 2021 Community Survey!
Este site não é mais atualizado.Vá para pt-br.react.dev

Notas de Implementação

Esta seção é um conjunto de notas de implementação para o reconciliador de pilha.

Ela é bastante técnica e assume um forte entendimento da API pública do React, assim como da sua divisão em núcleos, renderizadores e o próprio reconciliador. Se você não estiver muito familiarizado com o código do React, leia a visão geral da base de código primeiro.

Também é pressuposto o entendimento da diferença entre componentes React, suas instâncias e elementos.

O reconciliador de pilha foi usado no React 15 e em versões anteriores. Está localizado em src/renderers/shared/stack/reconciler.

Vídeo: Construindo React do zero

Paul O’Shannessy deu uma palestra sobre construir React do zero que muito inspirou esse documento.

Tanto este texto quanto a palestra são simplificações da real base de código, então se familiarizar com os dois pode resultar em um entendimento melhor.

Visão geral

O reconciliador em si não possui uma API pública. Renderizadores como o React DOM e React Native usam-no para atualizar a interface do usuário de acordo com os componentes React escritos pelo usuário.

Montagem como um Processo Recursivo

Vamos considerar a primeira vez que você monta um componente:

const root = ReactDOM.createRoot(rootEl);
root.render(<App />);

root.render passará <App /> para o reconciliador. Lembre-se que <App /> é um elemento React, ou seja, uma descrição do o que renderizar. Você pode pensar nisso como um objeto simples:

console.log(<App />);
// { type: App, props: {} }

O reconciliador irá verificar se App é uma classe ou uma função.

Se App for uma função, o reconciliador chamará App(props) para obter o elemento renderizado.

Se App for uma classe, o reconciliador instanciará App com new App(props), chamará o método de ciclo de vida componentWillMount(), e por fim chamando o método render()` para obter o elemento renderizado.

De qualquer forma, o reconciliador saberá em que elemento o App foi “renderizado”.

Esse processo é recursivo. App talvez seja renderizado para um <Greeting />, Greeting talvez seja renderizado para um <Button />, e assim por diante. O reconciliador irá “investigar” os componentes definidos pelo usuário recursivamente enquanto ele aprende para o quê cada um será renderizado.

Você pode imaginar esse processo como um pseudo-código:

function isClass(type) {
  // Subclasses React.Component possuem essa flag
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// Essa função recebe um elemento React (e.g. <App />)
// e retorna um DOM ou nó Nativo representando a árvore montada.
function mount(element) {
  var type = element.type;
  var props = element.props;

  // Nós vamos determinar o elemento renderizado
  // executando o tipo como função
  // ou criando uma instância e chamando render().
  var renderedElement;
  if (isClass(type)) {
    // Componente de classe
    var publicInstance = new type(props);
    // Define as props
    publicInstance.props = props;
    // Chama o ciclo de vida se necessário
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    // Obtêm o elemento renderizado ao chamar render()
    renderedElement = publicInstance.render();
  } else {
    // Componente de função
    renderedElement = type(props);
  }

  // Esse processo é recursivo pois um componente pode
  // retornar um elemento com o tipo de outro componente.
  return mount(renderedElement);

  // Nota: essa implementação é incompleta e recorre infinitamente!
  // Ela só lida com elementos <App /> ou <Button />.
  // Ela não lida com elementos como <div /> ou <p /> ainda.
}

var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);

Nota:

Isso realmente é um pseudocódigo. Não é semelhante a implementação real. Causará um estouro de pilha porque não discutimos quando parar a recursão.

Recapitulando alguns conceitos chaves do exemplo acima:

  • Os elementos do React são objetos simples que representam o tipo do componente (e.g. App) e as props.
  • Componentes definidos pelo usuário (e.g. App) podem ser classes ou funções mas todos eles “se renderizam” a um elemento.
  • “Montagem” é um processo recursivo que cria uma árvore DOM ou Nativa dado um elemento React de nível superior (e.g. <App />).

Montando Elementos Hospedeiros

Esse processo seria inútil se o resultado não fosse renderizar algo na tela.

Além dos componentes definidos pelo usuário (“compostos”), elementos React podem também representar componentes (“hospedeiros”) para plataformas específicas. Por exemplo, Button pode retornar uma <div /> no seu método render.

Se a propriedade type for uma string, estamos lidando com um elemento hospedeiro:

console.log(<div />);
// { type: 'div', props: {} }

Não há código definido pelo usuário associado com elementos do tipo hospedeiro.

Quando o reconciliador encontra um elemento hospedeiro, ele permite que o renderizador cuide da montagem. Por exemplo, o React DOM criaria um nó do DOM.

Se o elemento hospedeiro possuir filhos, o reconciliador recursivamente os monta seguindo o mesmo algoritmo descrito acima. Não importa se os filhos são hospedeiros (como <div><hr /></div>) ou se são compostos (como <div><Button /></div>), ou os dois.

Os nós DOM produzidos pelos componentes filhos serão anexados ao nó DOM pai, e, recursivamente, a completa estrutura DOM será construída.

Nota:

O reconciliador em si não está ligado ao DOM. O exato resultado da montagem (por vezes chamada de “mount image” no código fonte) depende do renderizador, e pode ser um nó do DOM (React DOM), uma string (React DOM Server), ou um número representando uma view nativa (React Native).

Se fôssemos estender o código para lidar com elementos hospedeiros, ficaria assim:

function isClass(type) {
  // Subclasses React.Component possuem essa flag
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// Essa função apenas lida com elementos do tipo composto.
// Por exemplo, ela lida com <App /> e <Button />, mas não com uma <div />.
function mountComposite(element) {
  var type = element.type;
  var props = element.props;

  var renderedElement;
  if (isClass(type)) {
    // Componente de classe
    var publicInstance = new type(props);
    // Define as props
    publicInstance.props = props;
    // Chama o ciclo de vida se necessário
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    renderedElement = publicInstance.render();
  } else if (typeof type === 'function') {
    // Componente de função
    renderedElement = type(props);
  }

  // Isso é recursivo mas eventualmente chegaremos no fim da recursão quando
  // o elemento for o hospedeiro (e.g. <div />) ao invés de composto (e.g. <App />):
  return mount(renderedElement);
}

// Essa função apenas lida com elementos do tipo hospedeiro.
// Por exemplo, ela lida com <div /> e <p /> mas não com um <App />
function mountHost(element) {
  var type = element.type;
  var props = element.props;
  var children = props.children || [];
  if (!Array.isArray(children)) {
    children = [children];
  }
  children = children.filter(Boolean);

  // Esse bloco de código não deveria estar no reconciliador.
  // Renderizadores diferentes podem inicializar nós diferentemente.
  // Por exemplo, React Native iria criar views de iOS ou Android.
  var node = document.createElement(type);
  Object.keys(props).forEach(propName => {
    if (propName !== 'children') {
      node.setAttribute(propName, props[propName]);
    }
  });

  // Monta os filhos
  children.forEach(childElement => {
    // Filhos podem ser hospedeiros (e.g. <div />) ou compostos (e.g <Button />).
    // Também os montaremos recursivamente:
    var childNode = mount(childElement);

    // Essa linha de código também é específica do renderizador.
    // Ela seria diferente dependendo do renderizador:
    node.appendChild(childNode);
  });

  // Retorna o nó do DOM como resultado da montagem.
  // Aqui é onde a recursão acaba.
  return node;
}

function mount(element) {
  var type = element.type;
  if (typeof type === 'function') {
    // Componentes definidos pelo usuário
    return mountComposite(element);
  } else if (typeof type === 'string') {
    // Componentes de plataformas específicas
    return mountHost(element);
  }
}

var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);

Isto funciona mas ainda está longe de como o reconciliador é realmente implementado. O ingrediente que falta é o suporte para atualizações.

Introduzindo instâncias Internas

A característica principal do React é que você pode re-renderizar tudo, e ele não irá recriar o DOM ou resetar o estado.

root.render(<App />);
// Deve reutilizar o DOM existente:
root.render(<App />);

Contudo, a nossa implementação acima apenas sabe como montar a árvore inicial. Ela não executa atualizações na árvore pois não armazena todas as informações necessárias, como todas as publicInstances, ou quais nós do DOM correspondem a qual componente.

O código do reconciliador de pilha resolve isso fazendo a função mount() um método e a colocando em uma classe. Existem desvantagens para essa abordagem, e nos iremos na direção oposta na atual reescrita do reconciliador. No entanto, é assim que funciona atualmente.

Ao invés de funções mountHost e mountComposite separadas, nós criaremos duas classes: DOMComponent e CompositeComponent.

Ambas as classes possuem um construtor aceitando o element, assim como um método mount() retornando o nó montado. Nós iremos trocar a função de nível superior mount() com uma factory que instancia a classe correta.

function instantiateComponent(element) {
  var type = element.type;
  if (typeof type === 'function') {
    return new CompositeComponent(element);
    // Componentes definidos pelo usuário
  } else if (typeof type === 'string') {
    // Componentes de plataformas específicas
    return new DOMComponent(element);
  }  
}

Primeiro, vamos considerar a implementação de CompositeComponent:

class CompositeComponent {
  constructor(element) {
    this.currentElement = element;
    this.renderedComponent = null;
    this.publicInstance = null;
  }

  getPublicInstance() {
    // Para componentes compostos, exponha a instância da classe.
    return this.publicInstance;
  }

  mount() {
    var element = this.currentElement;
    var type = element.type;
    var props = element.props;

    var publicInstance;
    var renderedElement;
    if (isClass(type)) {
      // Componente de classe
      publicInstance = new type(props);
      // Define as props
      publicInstance.props = props;
      // Chama o ciclo de vida se necessário
      if (publicInstance.componentWillMount) {
        publicInstance.componentWillMount();
      }
      renderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      // Componente de função
      publicInstance = null;
      renderedElement = type(props);
    }

    // Salva a instância pública
    this.publicInstance = publicInstance;

    // Instancia a instância interna filha de acordo com o elemento.
    // Seria algo como um DOMComponent para <div /> ou <p />,
    // e um CompositeComponent para <App /> ou <Button />:
    var renderedComponent = instantiateComponent(renderedElement);
    this.renderedComponent = renderedComponent;

    // Monta o output renderizado
    return renderedComponent.mount();
  }
}

Isso não é muito diferente da nossa implementação anterior de mountComposite, mas agora podemos salvar algumas informações, como this.currentElement, this.renderedComponent, e this.publicInstance , para usar durante atualizações.

Note que uma instância de CompositeComponent não é a mesma coisa que uma instância de um element.type fornecida pelo usuário. CompositeComponent é um detalhe de implementação do nosso reconciliador e nunca é exposto para o usuário. A classe definida pelo usuário é quem lê de element.type e CompositeComponent cria uma instância dela.

Para evitar confusão, nós vamos chamar instâncias de CompositeComponent e DOMComponent de “instâncias internas”. Elas existem para que possamos associá-las a alguns dados de longa vida. Apenas o renderizador e o reconciliador sabem que elas existem.

Em contraste, nós chamamos uma instância de uma classe definida pelo usuário uma “instância pública”. A instância pública é o que você vê como this no render() e outros métodos de seus componentes customizados.

A função mountHost(), refatorada para ser um método mount() na classe DOMComponent, também é familiar:

class DOMComponent {
  constructor(element) {
    this.currentElement = element;
    this.renderedChildren = [];
    this.node = null;
  }

  getPublicInstance() {
    // For DOM components, only expose the DOM node.
    return this.node;
  }

  mount() {
    var element = this.currentElement;
    var type = element.type;
    var props = element.props;
    var children = props.children || [];
    if (!Array.isArray(children)) {
      children = [children];
    }

    // Cria e salva o nó
    var node = document.createElement(type);
    this.node = node;

    // Define os atributos
    Object.keys(props).forEach(propName => {
      if (propName !== 'children') {
        node.setAttribute(propName, props[propName]);
      }
    });

    // Cria e salva os filhos contidos.
    // Cada um deles pode ser um DOMComponent ou um CompositeComponent
    // dependendo se o tipo do elemento é uma string ou uma função.
    var renderedChildren = children.map(instantiateComponent);
    this.renderedChildren = renderedChildren;

    // Coleta nos DOM retornados na montagem
    var childNodes = renderedChildren.map(child => child.mount());
    childNodes.forEach(childNode => node.appendChild(childNode));

    // Retorna o nó do DOM como resultado da montagem
    return node;
  }
}

A diferença principal depois de refatorar mountHost() é que agora nós podemos deixar this.node e this.renderedChildren associados com a instância interna do componente DOM. Nós também os usaremos para aplicar atualizações não destrutivas no futuro.

Como resultado, cada instância interna, composta ou hospedeira, agora aponta para sua instância interna filha. Para auxiliar na visualização disso, se o componente de função <App> renderiza um componente de classe, e a classe Button renderiza a <div>, a árvore da instância interna ficaria assim:

[object CompositeComponent] {
  currentElement: <App />,
  publicInstance: null,
  renderedComponent: [object CompositeComponent] {
    currentElement: <Button />,
    publicInstance: [object Button],
    renderedComponent: [object DOMComponent] {
      currentElement: <div />,
      node: [object HTMLDivElement],
      renderedChildren: []
    }
  }
}

No DOM você apenas veria a <div>. No entanto, a árvore da instância interna possui ambas instâncias internas: composta e hospedeira.

A instância interna composta precisa armazenar:

  • O elemento atual.
  • A instância pública se o tipo do elemento for uma classe.
  • A instância interna única renderizada. Pode ser tanto um DOMComponent ou um CompositeComponent.

A instância interna hospedeira precisa armazenar:

  • O elemento atual.
  • O nó do DOM.
  • Todas as instâncias internas filhas. Cada uma delas pode ser tanto um DOMComponent ou um CompositeComponent.

Se você está tendo dificuldades para imaginar como uma árvore de instâncias internas é estruturada em aplicações mais complexas, React DevTools pode te dar uma boa aproximação, pois instâncias hospedeiras são marcadas com cinza, e instâncias compostas com roxo: