We want to hear from you!Take our 2021 Community Survey!
Este sitio ya no se actualiza.Vaya a react.dev

Notas de implementación

Esta sección es una colección de notas de implementación para el reconciliador de pila.

Es muy técnica y asume un gran entendimiento de la API pública de React como también sobre la división de React en núcleo, renderizadores y el reconciliador. Si no estás muy familiarizado con la base de código de React, primero lee la visión general de la base de código.

Además se asume una buena comprensión de las diferencias entre componentes de React, sus instancias y sus elementos.

El reconciliador de pila se usó en React 15 y también en versiones anteriores. Está ubicado en src/renderers/shared/stack/reconciler.

Video: Construyendo React desde 0

Paul O’Shannessy dio una charla sobre construir React desde 0 que inspiró este documento.

Tanto este documento como su charla son simplificaciones de la base de código real por lo que obtendrás un mejor entendimiento familiarizándote con ambos.

Visión general

El reconciliador por sí mismo no tiene una API pública. Los renderizadores como React DOM y React Native lo usan para actualizar de manera eficiente la interfaz de usuario acorde a los componentes de React diseñados por el usuario.

El montaje como un proceso recursivo

Consideremos la primera vez que montas un componente:

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

root.render pasará <App /> al reconciliador. Recuerda que <App /> es un elemento de React, es decir, una descripción de qué hay que renderizar. Puedes pensarlo como si fuera un objecto simple:

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

El reconciliador comprobará si App es una clase o una función.

Si App es una función, el reconciliador llamará App(props) para renderizar el elemento.

Si App es una clase, el reconciliador instanciará una App con new App(props), llamará al método del ciclo de vida componentWillMount(), y por último llamará al método render() para obtener el elemento renderizado.

De cualquier manera, el reconciliador averiguará a qué elemento se renderizó App.

Este proceso es recursivo. App puede ser renderizado como <Greeting />, <Greeting /> puede ser renderizado como <Button />, y así sucesivamente. El reconciliador examinará a fondo a través de los componentes definidos por el usuario de manera recursiva a medida que averigua a qué se renderiza cada componente.

Puedes imaginar este proceso como pseudocódigo:

function isClass(type) {
  // Las subclases de React.Component tienen este indicador
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// Esta función toma un elemento de React (Por ej. <App />)
// y devuelve un nodo DOM o nativo que representa el árbol montado.
function mount(element) {
  var type = element.type;
  var props = element.props;

  // Determinaremos el elemento renderizado
  // ejecutando su tipo como una función
  // o creando una instancia y llamando a render().
  var renderedElement;
  if (isClass(type)) {
    // Clase componente
    var publicInstance = new type(props);
    // Establecer las props
    publicInstance.props = props;
    // Llamar al ciclo de vida si es necesario
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    // Obtener el elemento renderizado llamando a render()
    renderedElement = publicInstance.render();
  } else {
    // Función componente
    renderedElement = type(props);
  }

  // Este proceso es recursivo porque un componente
  // puede devolver un elemento con un tipo de otro componente.
  return mount(renderedElement);

  // Nota: ¡esta implementación está incompleta y se repite indefinidamente!
  // Solo acepta elementos como <App /> o <Button />.
  // Todavía no acepta elementos como <div /> o <p />.
}

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

Nota:

Esto realmente es pseudocódigo. No es similar a la implementación real. Además causará un desbordamiento de pila porque no hemos analizado cuando parar la recursividad.

Hagamos un repaso de algunas ideas clave con el ejemplo anterior:

  • Los elementos de React son objetos simples que representan el tipo de un componente (Por ej. App) y las props.
  • Los componentes definidos por el usuario (Por ej. App) pueden ser clases o funciones pero todos “se renderizan” como elementos.
  • El “montaje” es un proceso recursivo que crea un árbol DOM o nativo dado el elemento de React de nivel superior (Por ej. <App />).

Montaje de elementos anfitriones

Este proceso sería inservible si no renderizáramos algo en la pantalla como resultado.

Sumados a los componentes definidos por el usuario o (“compuestos”), los elementos de React también pueden representar componentes específicos a la plataforma o (“anfitriones”). Por ejemplo, Button puede devolver un <div /> desde su método render.

Si la propiedad type de un elemento es una string, sabemos que estamos trabajando con un elemento anfitrión:

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

No hay código definido por el usuario asociado con elementos anfitriones.

Cuando el reconciliador encuentra un elemento anfitrión, deja que el renderizador se encargue de montarlo. Por ejemplo, React DOM podría crear un nodo del DOM.

Si el elemento anfitrión tiene hijos, el reconciliador los monta de manera recursiva siguiendo el mismo algoritmo como en el caso anterior. No importa si los hijos son anfitriones (como <div><hr /></div>), compuestos (como <div><Button /></div>), o ambos.

Los nodos del DOM producidos por componentes hijos serán anexados al nodo padre del DOM, y recursivamente, la estructura completa del DOM será ensamblada.

Nota:

El reconciliador mismo no está ligado al DOM. El resultado exacto del montaje (a veces llamado “mount image” en el código fuente) depende del renderizador, y puede ser un nodo del DOM (React DOM), una string (React DOM Server), o un número representando una vista nativa (React Native).

Si fueramos a extender el código para aceptar elementos anfitriones, se vería así:

function isClass(type) {
  // Las subclases de React.Component tienen este indicador
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// Esta función sólo acepta elementos de tipo compuesto.
// Por ejemplo, acepta <App /> y <Button />, pero no <div />.
function mountComposite(element) {
  var type = element.type;
  var props = element.props;

  var renderedElement;
  if (isClass(type)) {
    // Clase componente
    var publicInstance = new type(props);
    // Establecer las props
    publicInstance.props = props;
    // Llamar al ciclo de vida si es necesario
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    renderedElement = publicInstance.render();
  } else if (typeof type === 'function') {
    // Función Componente
    renderedElement = type(props);
  }
  // Esto es recursivo pero eventualmente alcanzaremos el final de la recursión
  // cuando el elemento sea anfitrión (Por ej. <div /> en vez de compuesto (Por ej. <App />):
  return mount(renderedElement);
}

// Esta función solo acepta elementos de tipo anfitrión.
// Por ejemplo, acepta <div /> y <p /> pero no <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);

  // Este bloque de código no debería estar en el reconciliador.
  // Diferentes renderizadores podrían inicializar nodos de manera diferente.
  // Por ejemplo, React Native crearía vistas para iOS o Android.
  var node = document.createElement(type);
  Object.keys(props).forEach(propName => {
    if (propName !== 'children') {
      node.setAttribute(propName, props[propName]);
    }
  });

  // Montaje de los hijos
  children.forEach(childElement => {
    // Los hijos pueden ser anfitriones (Por ej. <div />) o compuestos (Por ej. <Button />)
    // También los montaremos de manera recursiva:
    var childNode = mount(childElement);

    // Esta línea de código también es específica a cada renderizador.
    // Sería diferente dependiendo del renderizador:
    node.appendChild(childNode);
  });

  // Devolver el nodo del DOM como resultado del montaje.
  // Aquí es donde la recursión finaliza.
  return node;
}

function mount(