Avatar image from Alan's blog

AlanJS.dev

Back
[ES - SP] Probablemente no sepas react
React logo icon

Warning

Este post se realizó con la versión 18 de react, por lo que algunos conceptos pueden variar en versiones anteriores y futuras

[ES - SP] Probablemente no sepas react

Este post tratará sobre el funcionamiento interno de React, intentaré ser lo más conciso posible dentro del marco de lo posible para no confundir ni entrar en detalles super técnicos, aunque a veces me será inevitable

¿Cómo funciona React?

En primer lugar una vez que cargamos react en nuestro sitio web, el JavaScript setea valores por necesarios para que React funcione, un ejemplo de esto pueden ser las lanes, que funciona un papel muy importante en la arquitectura de React. Una vez que se setea todas las flags, variables, objetos y funciones para que React funcione, la guía de React nos dice que hagamos uso de createRoot() (esto según la versión 18 de React, que es en la que basamos esta presentación)

¿Cómo funciona entonces createRoot?

En esencia, no quiero volver más complejo de lo que es un tema que ya de por sí tiene suficiente complejidad, asi que voy a intentar mantenerlo simple. React necesita un elemento DOM llamado raíz para construir el árbol de DOM virtual que utiliza React por debajo, pero cabe destacar que en las anteriores versiones no existía el createRoot como tal, y no es simplemente una función que se utilizó como alias para reemplazar el render de las versiones anteriores, sino que createRoot habilita lo que se denomina una raíz concurrente, que no es ni nada más ni nada menos que habilitar las funciones de concurrencia para la raíz actual seleccionada, de esta manera podemos disfrutar de los beneficios como los algoritmos de priorización de tareas en nuestra aplicación.

Prácticamente lo que createRoot hace es:

  1. Valida si el contenedor que le pasamos es un elemento válido para ser utilizado como contenedor
  2. Crea un contenedor createContainer
  3. Marca el contenedor como root, es decir como raíz del árbol
  4. Escucha TODOS los eventos soportados en la raíz del árbol (Por favor, ver Events antes de revisar esta sección)
  5. Retorna la creación de una instancia ReactDOMRoot (que en esencia es una FiberRoot)

Con lo cual el código de createRoot podría verse algo así:

const ConcurrentRoot = 1; // Una flag que nos servirá para habilitar la concurrencia
function createRoot(container) {
// Validar el contenedor
// Crear el contenedor
// Marcas el contenedor como el root de la app
// Escuchar TODOS los eventos en el root
// Devolver una instancia de ReactDOMRoot
//if(!isValidContainer(container)) throw new Error ("The container is not a valid DOM element");
//const root = createContainer(container, ConcurrentRoot);
//markContainerAsRoot(root.current, container);
//listenToAllSupportedEvents(root);
//return new ReactDOMRoot(root);
}
/*=========================*/
const randomKey = Math.random().toString(36).slice(2);
const randomKeyIdentifier = '__reactContainer$' + randomKey;
// Simplemente un constructor
function ReactDOMRoot(internalRoot) {
this._internalRoot = internalRoot;
}
// La función render que utilizamos
ReactDOMRoot.prototype.render = function() {
console.log("rendering logic");
}
// Marca el container como el root dejando un identifier en las propiedades del nodo HTML
function markContainerAsRoot(hostRoot, realDOMNode) {
node[randomKeyIdentifier] = hostRoot;
}
// La lógica para crear el contenedor
function createContainer(node, ConcurrentRoot) {
console.log("creating container logic");
return node;
}
// Lógica para escuchar TODOS los eventos en el root
function listenToAllSupportedEvents(root) {
console.log("adding listener to all supported events");
}

Validation del container

La validación del contenedor es algo sencillo, solamente hace validaciones básicas, en código entonces tendríamos lo siguiente:

// Guardamos los números que representan a cada nodo
const ELEMENT_NODE = 1;
const DOCUMENT_NODE = 9;
const DOCUMENT_FRAGMENT_NODE = 11;
const ConcurrentRoot = 1; // Una flag que nos servirá para habilitar la concurrencia
function isValidContainer(realDOMNode) {
return !!(node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE));
}
function createRoot(container) {
if(!isValidContainer(container)) throw new Error ("The container is not a valid DOM element");
//const root = createContainer(container, ConcurrentRoot);
//markContainerAsRoot(root.current, container);
//listenToAllSupportedEvents(root);
//return new ReactDOMRoot(root);
}

Creación del root container para React

El código que se encarga sobre la creación del root container para react es algo similar a este:

function createContainer(containerInfo, tag) {
const initialChildren = false;
return createFiberRoot(containerInfo, tag);
}

El initial children debe ser false debido a que por defecto la root no tendrá ningún hijo, lo que en realidad quiere decir es que muy posiblemente el componente (típicamente denominado como:) <App /> será su hijo, pero eso se lo especificaremos con root.render(<App />).

Por lo visto la función no es muy descriptiva en sí misma, ya que devuelve el valor retornado por la función createFiberRoot, asi que podemos aprovechar para ver el código de la función createFiberRoot (cabe destacar nuevamente, que esta es una versión muy simplificada):

/*
Identificadores internos de react con sus respectivos números:
const FunctionComponent = 0;
const ClassComponent = 1;
const IndeterminateComponent = 2; // Before we know whether it is function or class
const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
const HostComponent = 5;
const HostText = 6;
const Fragment = 7;
const Mode = 8;
const ContextConsumer = 9;
const ContextProvider = 10;
const ForwardRef = 11;
const Profiler = 12;
const SuspenseComponent = 13;
const MemoComponent = 14;
const SimpleMemoComponent = 15;
const LazyComponent = 16;
const IncompleteClassComponent = 17;
const DehydratedFragment = 18;
const SuspenseListComponent = 19;
const ScopeComponent = 21;
const OffscreenComponent = 22;
const LegacyHiddenComponent = 23;
const CacheComponent = 24;
const TracingMarkerComponent = 25;
*/
// En este caso vamos a usar el HostRoot
const HostRoot = 3;
function FiberRoot(containerInfo, concurrentMode) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
/*Muchas más propiedades necesarias y útiles que necesita react*/
}
function FiberNode(tag, pendingProps, key) {
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null; // Esto tiene que ser una fibra
this.return = null;
this.child = null;
this.sibling = null;
/*Muchas más propiedades necesarias y útiles que necesita react para las fibras*/
}
function createFiber(tag, pendingProps, key) {
return new FiberNode(tag, pendingProps, key)
}
function createFiberRoot(containerInfo, concurrentMode, initialChildren) {
// Crear una FiberRoot que le permitirá a react crear un árbol de Fibras (como el DOM, pero en Fibras)
const root = new FiberRoot(containerInfo, concurrentMode);
const uninitializedFiber = createFiber(concurrentMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// Solamente para que sepas, en este apartado iría mucha más lógica de react
// como por ejemplo la lógica de un caché interno que utiliza, y las memoizaciones
// que no serán el tema principal de este post. Además también react inicializará
// una cola de actualizaciones para las fibras que aún están sin inicializar
return root;
}

La función createFiberRoot se encarga de generar la raíz de un árbol de Fibras o Fibers (no importa aún si no entendemos la totalidad de los conceptos, los abordaremos en un momento).

Fibras o Fibers

Si tuviese que elegir que entiendas solamente algo de todo este, sin lugar a dudas serían las fibras, las fibras en React tienen una importancia FUNDAMENTAL. El objetivo principal de las Fibras es permitir que React pueda pausar, reanudar y reutilizar el trabajo de renderización según sea necesario para mejorar el rendimiento

Pero… ¿Qué son? Fundamentalmente, una Fibra en React es como la unidad mínima de trabajo, similar a lo que es un átomo para una molécula. Puede ser un concepto abstracto, pero vamos a intentar ilustrarlo con un ejemplo más tangible y práctico: imaginemos un partido de fútbol.

En un partido, cada jugador sigue un conjunto de instrucciones y reacciona a situaciones en el partido en tiempo real. Supongamos que un hincha o intruso ingresa inesperadamente a la cancha. Al igual que en React, donde una actualización puede interrumpir el proceso de renderización, el juego se detiene. El personal de seguridad activa un protocolo para manejar la situación, similar a cómo React manejaría un evento de usuario o una actualización de estado con alta prioridad.

Mientras tanto, los árbitros controlan la situación, tomando decisiones y posiblemente deteniendo temporalmente el partido, como lo haría React al pausar el trabajo en curso. Los jugadores, por su parte, dejan de jugar siguiendo las reglas del juego, sabiendo que cualquier acción durante la pausa no cuenta para el resultado final. Es decir, las fibras en esta caso, serían el conjunto de instrucciones que tiene cada persona participe en el partido, y que sabe cómo reaccionar ante determinada situación.

Incluso puede pasar que el equipo de seguridad intercepte antes al intruso de que este ingrese al campo propiamente dicho, con lo cual no existiría la necesidad de pausar el partido. En tal caso el árbitro sabe que no es necesario pausar el partido porque no afecta en nada al juego, y lo sabe gracias a que el equipo de seguridad (la unidad de trabajo - la fibra -) logró interceptar al intruso.

Las fibras para React pueden ser procesos lógicos de trabajo, componentes funcionales (si, las fibras no solo son componentes), componentes basados en clase, fragmentos, textos, etc.

Siguiendo con este ejemplo, podríamos decir que los jugadores son los componentes, y cada jugador tiene su plan de juego (su proceso lógico mental de cómo jugar el partido) según su rol, podríamos decir entonces que un jugador tiene su equivalente en una fibra. Pero el árbitro también (pese a no ser un componente) debe tener su equivalente en una Fibra, ya que también tiene un plan, y si el árbitro realiza determinadas acciones debe informar al resto (por ejemplo a los jugadores), esto quiere decir que cada Fibra debe actuar en consecuencia a los cambios.

Como podemos ver es más un concepto, no podemos establecer una analogía directa con algo real, de la vida cotidiana.

Marcar un contenedor como el root

En este paso simplemente se le pone al nodo (DOM real) un identificador interno de react para reconocerlo como una raíz, la función sería algo como esta:

const randomKey = Math.random().toString(36).slice(2);
const randomKeyIdentifier = '__reactContainer$' + randomKey;
// Marca el container como el root dejando un identifier en las propiedades del nodo HTML
function markContainerAsRoot(hostRoot, realDOMNode) {
realDOMNode[randomKeyIdentifier] = hostRoot;
}

Events (función listenToAllSupportedEvents)

  1. React separa en dos tipos los eventos, los delegados (los que hacen bubbling) y los no delegados (los que no hacen bubbling), es importante mencionar que de todas maneras, todos los eventos nativos tienen una fase de capturing.
  2. ¿Por qué React escucha TODOS los eventos nativos? React tiene una manera muy ingeniosa de manejar los eventos, debido a que en javascript los eventos hacen bubbling y/o capturing react decide aprovechar este comportamiento para que en lugar de escuchar eventos individuales se escuchen eventos en un nivel superior de esta manera no necesita agregar múltiples eventos para diferentes elementos, obteniendo mejoras en la performance.
    1. El problema con este approach es que no todos los eventos hacen bubbling, con lo cual react crea un Set de estos eventos NO DELEGADOS (eventos que no hacen bubbling) y los agrega solamente en la etapa de capturing. Y a los eventos que hacen bubbling los escucha tanto en su fase de capture como en su etapa de bubble.
  3. Para aclarar sobre el ejemplo anterior react hace algo similar (el código fuente contiene mucha más lógica) a esto:
    const allNativeEvents = new Set()
    function registerEvents() {
    // Lógica para listar todos los eventos e incorporarlos al set de allNativeEvents
    const events = ['abort', 'auxClick', 'cancel', 'canPlay', 'canPlayThrough', 'click', 'close', 'contextMenu', 'copy', 'cut', 'drag', 'click']; // etc...
    for (let i = 0; i < events.length; i++) {
    allNativeEvents.add(events[i]);
    }
    }
    function listenToNativeEvent(eventName, isCapturePhase, element) {
    if (isCapturePhase) {
    // Agregar event listener para la fase de captura
    element.addEventListener(eventName, eventHandler, true);
    } else {
    // Agregar event listener para la fase de burbuja
    element.addEventListener(eventName, eventHandler, false);
    }
    }
    // Función que itera sobre los eventos y decide cómo agregar los listeners
    function listenToAllSupportedEvents(rootContainerElement) {
    allNativeEvents.forEach(function (domEventName) {
    if (domEventName !== 'selectionchange') {
    // Si el evento no está en el conjunto de no delegados, agregar para bubbling
    // es decir, si es un evento delegado entonces agregar para la fase de bubbling
    if (!nonDelegatedEvents.has(domEventName)) {
    listenToNativeEvent(domEventName, false, rootContainerElement);
    }
    // Agregar event listener para la fase de captura en todos los casos
    listenToNativeEvent(domEventName, true, rootContainerElement);
    }
    });
    }
  4. ¿En qué se diferencia el código fuente? Mi idea es que tengas un conocimiento sólido sobre React pero sin que sea entremos en tantos tecnisismos (aunque para entender este tópico me resulta imposible no usar algunos), pero me veo en la necesidad de explayarme aquí. React no simplemente agrega eventos, también los agrega separándolos en eventos activos y pasivos, luego les otorga un prioridad. Una imagen vale más que mil palabras:

How react handle the events

ReactDOMRoot

Lo que se hace una vez que se crea una instancia de ReactDOMRoot es setear internamente, en React, la raíz, pero esta vez no es un nodo del DOM real, sino la raíz una vez se tiene su equivalencia en Fibra. El código sería algo así:

// Simplemente un constructor
function ReactDOMRoot(internalRoot) {
this._internalRoot = internalRoot;
}
// La función render que utilizamos
ReactDOMRoot.prototype.render = function() {
console.log("rendering logic");
}

Como podemos observar, además, agrega un prototipo render, este es debido a que en algún punto de nuestra aplicación de react, haremos uso de:

root.render()

Código final de createRoot

Hasta ahora nuestro código quedaría de esta manera:

const ConcurrentRoot = 1; // Una flag que nos servirá para habilitar la concurrencia
const randomKey = Math.random().toString(36).slice(2);
const randomKeyIdentifier = '__reactContainer$' + randomKey;
const HostRoot = 3;
const ELEMENT_NODE = 1;
const DOCUMENT_NODE = 9;
const DOCUMENT_FRAGMENT_NODE = 11;
const allNativeEvents = new Set()
function isValidContainer(realDOMNode) {
return !!(node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE));
}
function FiberRoot(containerInfo, concurrentMode) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
/*Muchas más propiedades necesarias y útiles que necesita react*/
}
function FiberNode(tag, pendingProps, key) {
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null; // Esto tiene que ser una fibra
this.return = null;
this.child = null;
this.sibling = null;
/*Muchas más propiedades necesarias y útiles que necesita react para las fibras*/
}
function createFiber(tag, pendingProps, key) {
return new FiberNode(tag, pendingProps, key)
}
function createFiberRoot(containerInfo, concurrentMode, initialChildren) {
// Crear una FiberRoot que le permitirá a react crear un árbol de Fibras (como el DOM, pero en Fibras)
const root = new FiberRoot(containerInfo, concurrentMode);
const uninitializedFiber = createFiber(concurrentMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// Solamente para que sepas, en este apartado iría mucha más lógica de react
// como por ejemplo la lógica de un caché interno que utiliza, y las memoizaciones
// que no serán el tema principal de este post. Además también react inicializará
// una cola de actualizaciones para las fibras que aún están sin inicializar
return root;
}
// Simplemente un constructor
function ReactDOMRoot(internalRoot) {
this._internalRoot = internalRoot;
}
// La función render que utilizamos
ReactDOMRoot.prototype.render = function() {
console.log("rendering logic");
}
// Marca el container como el root dejando un identifier en las propiedades del nodo HTML
function markContainerAsRoot(hostRoot, realDOMNode) {
node[randomKeyIdentifier] = hostRoot;
}
// La lógica para crear el contenedor
function createContainer(containerInfo, tag) {
const initialChildren = false;
return createFiberRoot(containerInfo, tag);
}
function registerEvents() {
// Lógica para listar todos los eventos e incorporarlos al set de allNativeEvents
const events = ['abort', 'auxClick', 'cancel', 'canPlay', 'canPlayThrough', 'click', 'close', 'contextMenu', 'copy', 'cut', 'drag', 'click']; // etc...
for (let i = 0; i < events.length; i++) {
allNativeEvents.add(events[i]);
}
}
function listenToNativeEvent(eventName, isCapturePhase, element) {
if (isCapturePhase) {
// Agregar event listener para la fase de captura
element.addEventListener(eventName, eventHandler, true);
} else {
// Agregar event listener para la fase de burbuja
element.addEventListener(eventName, eventHandler, false);
}
}
// Función que itera sobre los eventos y decide cómo agregar los listeners
function listenToAllSupportedEvents(rootContainerElement) {
allNativeEvents.forEach(function (domEventName) {
if (domEventName !== 'selectionchange') {
// Si el evento no está en el conjunto de no delegados, agregar para bubbling
// es decir, si es un evento delegado entonces agregar para la fase de bubbling
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
// Agregar event listener para la fase de captura en todos los casos
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
}
// Lógica para escuchar TODOS los eventos en el root
function listenToAllSupportedEvents(root) {
console.log("adding listener to all supported events");
}
function createRoot(container) {
// Validar el contenedor
// Crear el contenedor
// Marcas el contenedor como el root de la app
// Escuchar TODOS los eventos en el root
// Devolver una instancia de ReactDOMRoot
if(!isValidContainer(container)) throw new Error ("The container is not a valid DOM element");
const root = createContainer(container, ConcurrentRoot);
markContainerAsRoot(root.current, container);
listenToAllSupportedEvents(root);
return new ReactDOMRoot(root);
}
registerEvents()

¿How the render works?

Usualmente nosotros realizamos el render de la siguiente manera

//...
root.render(<App />)
//...

Y es curioso porque jsx se encarga de interpretar lo anterior y transformarlo a javascript, a algo similar a esto:

root.render(React.createElement(App, null, undefined))

La realidad es que lo que se llama en las últimas versiones de react, es lo siguiente:

root.render(React.createElementWithValidation(App, null, undefined))

No quiero entrar en muchos detalles sobre la función de createElementWithValidation, porque hace algunas validaciones extras que no aportarán demasiado a nuestro entendimiento, pero se ve algo así:

function isValidType(type) {
if (typeof type === 'string' || typeof type === 'function') {
return true;
}
// Se hacen otras verificaciones como por ejemplo la del react fragment
return false;
}
function createElementWithValidation(type, props, children) {
const isValidType = isValidType(type);
// Se manejan los casos donde el elemento no sea válido
const element = createElement.apply(this, arguments)
if (element == null) {
return element;
}
if (validType) {
for (let i = 2; i < arguments.length; i++) {
// Valida las keys
validateChildKeys(arguments[i], type);
}
}
if (type === REACT_FRAGMENT_TYPE) {
// Valida las props del react fragment
validateFragmentProps(element);
}
return element;
}

Luego la función render que nosotros llamamos es la encargada de disparar todo el proceso del rendering.:

function updateContainer(element, container, parentComponent, callback) {
const current$1 = container.current; // This is the FiberNode, do not confuse with FiberRootNode
const eventTime = requestEventTime();
const lane = requestUpdateLane(current$1);
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
const update = createUpdate(eventTime, lane);
update.payload = {
element: element
};
callback = callback === undefined ? null : callback;
if (callback !== null) {
{
if (typeof callback !== 'function') {
error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
}
}
update.callback = callback;
}
const root = enqueueUpdate(current$1, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, current$1, lane, eventTime);
entangleTransitions(root, current$1, lane);
}
return lane;
}
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function (children) {
const root = this._internalRoot; // Esto contiene el FiberRootNode
const container = root.containerInfo;
updateContainer(children, root, null, null);
}

Aquí debemos hacer una pausa para explicar algunos temas importantes, como el Schedulery los timers que utiliza react, por lo que pudimmos ver no es simplemente actualizar el contenedor y pintar el contenido en la página, hay una serie de cosas por detrás antes de hacer una actualización en el DOM real. Antes de entrar en detalle me gustaría separar algunos algoritmos internos que maneja react.

¿Qué es el DOM virtual? Ya vimos con anterioridad que la principal estructura de dato interna, utilizada por React son las fibers o fibras. React construye un árbol de fibras sobre las que debe trabajar y es así cómo construye el DOM virtual.

La lógica antes de meternos de lleno en el Scheduler es obtener diferentes timers y asignar prioridades. Asi que no será de gran relevancia, pero luego vemos en el código otra función llamada requestEventTime, este timer es importantisimo y cumple una tarea vital en React, obtener el tiempo de la petición actual, de esta manera react sabe en todo momento cuándo se solicitó una actualización, de esta manera, puede llevar a cabo el resto de procesos. el código de requestEvenTime se ve así:

function requestEventTime() {
// If we are in the execution context and inside render or commit phase?
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return now();
} // We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== NoTimestamp) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
} // This is the first update since React yielded. Compute a new start time.
currentEventTime = now();
return currentEventTime;
}

Recordemos que se usan numeros binarios, para realizar las operaciones como las de RenderContext y CommitContext, por eso el uso de operadores binarios. currentEventTime es una variable global donde se guarda el tiempo donde la actualización fue pedida, y para eso React hace uso de la api de performance y del método now aunque si no está disponible usará Date.now().

Luego también tenemos la función requestUpdateLane(current$1) que se le pasa como argumento la Fibra. No vamos a entrar en detalles en esta función pero devuelve una lane para la actualización o evento actual, la primera vez que React ingresa a nuestra aplicación, es decir a su root, nunca existirá una actualización pendiente como tal, pero si un evento, en este caso el render se dispara en el DOMContentLoaded event, con lo cual se obtiene una Lane o Linea prioritaria, recordemos que las lanes en react funcionan como en los carriles de una autopista, a más chico es el número de lane, mayor será la prioridad de esa Lane, como en la vida real, mientras más a la izquierda el carril, a mayor urgencia circulará un vehículo por ese carril.

Con respecto a la función getContextForSubtree no vamos a estudiarla demasiado, simplemente es una manera de obtener y guardar el contexto basado en un subtree, el subtree en ese caso será el del padre hacia abajo..

Luego la función createUpdate que lo único que hace es crear un objeto con las propiedades necesarias para hacer el update, su código es el siguiente:

function createUpdate(eventTime, lane) {
let update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState,
payload: null,
callback: null,
next: null
};
return update;
}

Luego la función enqueueUpdate procede a colocar en la cola de actualización de la Fiber la actualización (sí, cada Fiber tiene su lista de actualizaciones, por eso dijimos con anterioridad que todas saben de alguna manera cómo reaccionar ante determinados cambios). No prestaremos demasiada atención a esta función pero si es parte vital de React.

Y finalmente llegamos a la función que le da vida a todo lo anterior, la función scheduleUpdateOnFiber que dentro, realiza algunas verificaciones, verifica si se programó una actualización durante la utilización de efectos, si existen actualizaciones anidadas, marca el root del FiberRoot actual como pendiente de actualización en el lane correspondiente, y otros procesos lógicos. Pero el más importante es que se asegura que el root se programe para una actualización es decir la función: ensureRootIsScheduled.

Scheduler

En esencia es la etapa donde React establece ciertas prioridades para programar determinadas tareas, como puede ser la de una actualización en el virtual DOM, pero aquí llega la sorpresa, React no realiza los cambios de manera directa hasta no tener algo certero en el DOM real, simplemente lo utiliza como referencia, asi que los cambios los realiza sobre el DOM virtual.

El scheduler es disparado por la función ensureRootIsScheduled, en esencia esta función gestiona las tareas de renderizado, y se asegura de que cada una de ellas se ejecuten con la prioridad adecuada, el código utiliza muchas funciones del core de React en las que no entraremos en detalle, pero el código es el siguiente:

function ensureRootIsScheduled(root, currentTime) {
let existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
let nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback$1(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// We use the highest priority lane to represent the priority of the callback.
let newCallbackPriority = getHighestPriorityLane(nextLanes);
// Check if there's an existing task. We may be able to reuse it.
let existingCallbackPriority = root.callbackPriority;
if (
existingCallbackPriority === newCallbackPriority &&
!(ReactCurrentActQueue$1.current !== null && existingCallbackNode !== fakeActCallbackNode)
) {
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback$1(existingCallbackNode);
}
// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
if (ReactCurrentActQueue$1.isBatchingLegacy !== null) {
ReactCurrentActQueue$1.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
{
// Flush the queue in a microtask.
if (ReactCurrentActQueue$1.current !== null) {
ReactCurrentActQueue$1.current.push(flushSyncCallbacks);
} else {
scheduleMicrotask(function () {
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
flushSyncCallbacks();
}
});
}
}
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediatePriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdlePriority;
break;
default:
schedulerPriorityLevel = NormalPriority;
break;
}
newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}

Como podemos ver, la función maneja todas las respectivas llamadas a lo que React denomina el Scheduler, por ahora nos enfocaremos en scheduleCallback$2 cuyo código es:

function scheduleCallback$2(priorityLevel, callback) {
{
// If we're currently inside an `act` scope, bypass Scheduler and push to
// the `act` queue instead.
let actQueue = ReactCurrentActQueue$1.current;
if (actQueue !== null) {
actQueue.push(callback);
return fakeActCallbackNode;
} else {
return scheduleCallback(priorityLevel, callback);
}
}
}

A este punto solo nos interesa saber que actQueue es una forma que tiene React para saltearse el flujo normal del Scheduler, por ahora nos vamos a seguir centrando en el Scheduler, eso nos lleva a la función scheduleCallback, y es en este punto donde de react-dom saltamos a la librería a secas, es decir a react, a la hora de hacer el export, react pone un alias a scheduleCallback, que en realidad hace referencia a unstable_scheduleCallback, en código se ve algo así:

// REACT-DOM
let scheduleCallback = React.unstable_scheduleCallback
// REACT
function unstable_scheduleCallback(priorityLevel, callback, options) {
let currentTime = getCurrentTime();
let startTime;
if (typeof options === 'object' && options !== null) {
let delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
let timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
let expirationTime = startTime + timeout;
let newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
} // Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}

Como podemos ver en el código, esta función maneja timers y solicita diferentes funcionalidades dependiendo de una serie de validaciones, pero la más importante es requestHostCallback:

let isMessageLoopRunning = false;
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}

Llegamos a otro punto intersante, debido a que aquí, React utiliza la Web API y usa la integración de MessageChannel. El valor de schedulePerformWorkUntilDeadline dependerá del browser donde se esté visitando el sitio, pero a nosotros nos interesa el caso de Chrome, no vamos a entrar en detalles como Server Side Rendering o SSG, ni tampoco en otro browser que no sea Chrome, asi que para este caso, la función toma el siguiente valor:

let channel = new MessageChannel();
let port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
let schedulePerformWorkUntilDeadline = function () {
port.postMessage(null);
};

No me voy a detener mucho tiempo en la API de MessageChannel pero en esencia, se usa debido a su naturaleza asíncrona y a que abre un canal de comunicación entre dos puertos, cada puerte puede enviar y “escuchar” mensajes, con lo cual, lo que está haciendo la función postMessage es disparar el listener de onmessage del port1, es decir que cuando se obtiene el mensaje (que solo incluye null, asi que solo se utiliza para disparar la función) se llama a la función performWorkUntilDeadline, React prefirió usar esta API en lugar de setTimeoutdebido a que setTimeoutsuele tener una demora de 4 segundos.

La función performWorkUntilDeadline disparará el workLoop más adelante, que es el loop que tiene react para disparar la etapa del renderizado, este el código de la función:

let performWorkUntilDeadline = function () {
if (scheduledHostCallback !== null) {
let currentTime = getCurrentTime(); // Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
let hasTimeRemaining = true; // If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `scheduledHostCallback` errors, then
// `hasMoreWork` will remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
} // Yielding to the browser will give it a chance to paint, so we can
};

Cabe destacar que scheduledHostCallback es en realidad un alias que se usa a nivel global, pero en realidad es una función callback, y toma el valor de la callback pasada através de este código:

// Globally...
let scheduledHostCallback = null;
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}

Asi que la callback en realidad es flushWork:

function flushWork(hasTimeRemaining, initialTime) {
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
let previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
let currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod code path.
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}

A su vez esta callback, que tiene por nombre flushWork llama al workloop del que hablamos previamente.

Render phase

Como vimos, la fase de renderizado está principalmente a cargo del denominado workLoop, que es el bucle de trabajo de React, hagamos de cuenta que un carpintero, tiene mucho trabajo por hacer, entonces decide dividir en una mesa, el trabajo que le queda por hacer, y en otra, su lugar de trabajo, donde está realizando el trabajo propiamente dicho, entonces cada vez que finaliza un trabajo, consulta en su reloj si terminó su jornada laboral, si aún tiene tiempo para seguir trabajando entonces, sigue con su próximo trabajo, caso contrario, deja el trabajo pendiente para su siguiente jornada laboral.

Cabe destacar que no hay que confundir la etapa de renderizado con el hecho de “imprimir los elementos en la pantalla”, no, la renderización es un proceso lógico, en el cual ordenaremos todos los elementos para que puedan ser pintados, además, react usa la estrategia de double-buffering para evitar que la experiencia de usuario sea mala, React necesita agregar y quitar regularmente elementos nuevos en la pantalla, con lo cual, muchas veces se puede producir un parpadeo al pintar los elementos, entonces la solución del double-buffering es perfecta, básicamente, hay dos buffers, uno el que el usuario puede ver, y otro por detrás de ese, donde se lleva a cabo el proceso de pintado, una vez que el pintado está listo, se intercambia, el que el usuario puede ver, de esta manera se evita tal parpadeo.

Volviendo a el workloop, cuyo código es el siguiente:

function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null && !(enableSchedulerDebugging )) {
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
let callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
let didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
let continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
} // Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
let firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}

Como verán, el código es en esencia lo explicado con el ejemplo del carpintero pero con un mayor nivel de abstracción, pueden interpretar el código por su cuenta pero en esencia nos quedaremos con esta parte:

let continuationCallback = callback(didUserCallbackTimeout);

Donde la callback es:

function performConcurrentWorkOnRoot(root, didTimeout) {
{
resetNestedUpdateFlag();
} // Since we know we're in a React event, we can clear the current
// event time. The next update will compute a new event time.
currentEventTime = NoTimestamp;
currentEventTransitionLane = NoLanes;
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
} // Flush any pending passive effects before deciding which lanes to work on,
// in case they schedule additional work.
let originalCallbackNode = root.callbackNode;
let didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
// Something in the passive effect phase may have canceled the current task.
// Check if the task node for this root was changed.
if (root.callbackNode !== originalCallbackNode) {
// The current task was canceled. Exit. We don't need to call
// `ensureRootIsScheduled` because the check above implies either that
// there's a new task, or that there's no remaining work on this root.
return null;
}
} // Determine the next lanes to work on, using the fields stored
// on the root.
let lanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
if (lanes === NoLanes) {
// Defensive coding. This is never expected to happen.
return null;
} // We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
let shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && ( !didTimeout);
let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
if (exitStatus !== RootInProgress) {
if (exitStatus === RootErrored) {
// If something threw an error, try rendering one more time. We'll
// render synchronously to block concurrent data mutations, and we'll
// includes all pending updates are included. If it still fails after
// the second attempt, we'll give up and commit the resulting tree.
let errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
}
}
if (exitStatus === RootFatalErrored) {
let fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended$1(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
//
// This should only happen during a concurrent render, not a discrete or
// synchronous update. We should have already checked for this when we
// unwound the stack.
markRootSuspended$1(root, lanes);
} else {
// The render completed.
// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
let renderWasConcurrent = !includesBlockingLane(root, lanes);
let finishedWork = root.current.alternate;
if (renderWasConcurrent && !isRenderConsistentWithExternalStores(finishedWork)) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes); // We need to check again if something threw
if (exitStatus === RootErrored) {
let _errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (_errorRetryLanes !== NoLanes) {
lanes = _errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, _errorRetryLanes); // We assume the tree is now consistent because we didn't yield to any
// concurrent events.
}
}
if (exitStatus === RootFatalErrored) {
let _fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended$1(root, lanes);
ensureRootIsScheduled(root, now());
throw _fatalError;
}
} // We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, lanes);
}
}
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}

Lo que nos interesa de este código es la función finishConcurrentRender:

function finishConcurrentRender(root, exitStatus, lanes) {
switch (exitStatus) {
case RootInProgress:
case RootFatalErrored:
{
throw new Error('Root did not complete. This is a bug in React.');
}
// Flow knows about invariant, so it complains if I add a break
// statement, but eslint doesn't know about invariant, so it complains
// if I do. eslint-disable-next-line no-fallthrough
case RootErrored:
{
// We should have already attempted to retry this tree. If we reached
// this point, it errored again. Commit it.
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
break;
}
case RootSuspended:
{
markRootSuspended$1(root, lanes); // We have an acceptable loading state. We need to figure out if we
// should immediately commit it or wait a bit.
if (includesOnlyRetries(lanes) && // do not delay if we're inside an act() scope
!shouldForceFlushFallbacksInDEV()) {
// This render only included retries, no updates. Throttle committing
// retries so that we don't show too many loading states too quickly.
let msUntilTimeout = globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now(); // Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
let nextLanes = getNextLanes(root, NoLanes);
if (nextLanes !== NoLanes) {
// There's additional work on this root.
break;
}
let suspendedLanes = root.suspendedLanes;
if (!isSubsetOfLanes(suspendedLanes, lanes)) {
// We should prefer to render the fallback of at the last
// suspended level. Ping the last suspended level to try
// rendering it again.
// FIXME: What if the suspended lanes are Idle? Should not restart.
var eventTime = requestEventTime();
markRootPinged(root, suspendedLanes);
break;
} // The render is suspended, it hasn't timed out, and there's no
// lower priority work to do. Instead of committing the fallback
// immediately, wait for more data to arrive.
root.timeoutHandle = scheduleTimeout(commitRoot.bind(null, root, workInProgressRootRecoverableErrors, workInProgressTransitions), msUntilTimeout);
break;
}
} // The work expired. Commit immediately.
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
break;
}
case RootSuspendedWithDelay:
{
markRootSuspended$1(root, lanes);
if (includesOnlyTransitions(lanes)) {
// This is a transition, so we should exit without committing a
// placeholder and without scheduling a timeout. Delay indefinitely
// until we receive more data.
break;
}
if (!shouldForceFlushFallbacksInDEV()) {
// This is not a transition, but we did trigger an avoided state.
// Schedule a placeholder to display after a short delay, using the Just
// Noticeable Difference.
// TODO: Is the JND optimization worth the added complexity? If this is
// the only reason we track the event time, then probably not.
// Consider removing.
let mostRecentEventTime = getMostRecentEventTime(root, lanes);
let eventTimeMs = mostRecentEventTime;
let timeElapsedMs = now() - eventTimeMs;
let _msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs; // Don't bother with a very short suspense time.
if (_msUntilTimeout > 10) {
// Instead of committing the fallback immediately, wait for more data
// to arrive.
root.timeoutHandle = scheduleTimeout(commitRoot.bind(null, root, workInProgressRootRecoverableErrors, workInProgressTransitions), _msUntilTimeout);
break;
}
} // Commit the placeholder.
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
break;
}
case RootCompleted:
{
// The work completed. Ready to commit.
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
break;
}
default:
{
throw new Error('Unknown root exit status.');
}
}
}

Que a su vez llama a la función commitRoot, que es si se quiere decir de alguna manera el corazón de React, porque sin esta función, nada de la renderización podría ser mostrado en nuestro sitio web.

Commit phase

En la etapa de commit se realiza el pintado y es un proceso síncrono, asi como la etapa del renderizado es asíncrona. La función commitRoot realiza algunos procesos para establecer algunas prioridades, pero inmediatamente llama a la función commitRootImpl, que es la que nos interesa, y su código es el siguiente:

function commitRootImpl(root, recoverableErrors, transitions, renderPriorityLevel) {
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
// passive effects. So we need to keep flushing in a loop until there are
// no more pending effects.
// TODO: Might be better if `flushPassiveEffects` did not automatically
// flush synchronous work at the end, to avoid factoring hazards like this.
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
flushRenderPhaseStrictModeWarningsInDEV();
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
let finishedWork = root.finishedWork;
let lanes = root.finishedLanes;
{
markCommitStarted(lanes);
}
if (finishedWork === null) {
{
markCommitStopped();
}
return null;
} else {
{
if (lanes === NoLanes) {
error('root.finishedLanes should not be empty during a commit. This is a ' + 'bug in React.');
}
}
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
if (finishedWork === root.current) {
throw new Error('Cannot commit the same tree as before. This error is likely caused by ' + 'a bug in React. Please file an issue.');
} // commitRoot never returns a continuation; it always finishes synchronously.
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackPriority = NoLane; // Update the first and last pending times on this root. The new first
// pending time is whatever is left on the root fiber.
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
markRootFinished(root, remainingLanes);
if (root === workInProgressRoot) {
// We can reset these now that they are finished.
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} // If there are pending passive effects, schedule a callback to process them.
// Do this as early as possible, so it is queued before anything else that
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes; // workInProgressTransitions might be overwritten, so we want
// to store it in pendingPassiveTransitions until they get processed
// We need to pass this through as an argument to commitRoot
// because workInProgressTransitions might have changed between
// the previous render and commit if we throttle the commit
// with setTimeout
pendingPassiveTransitions = transitions;
scheduleCallback$2(NormalPriority, function () {
flushPassiveEffects(); // This render triggered passive effects: release the root cache pool
// *after* passive effects fire to avoid freeing a cache pool that may
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
return null;
});
}
} // Check if there are any effects in the whole tree.
// TODO: This is left over from the effect list implementation, where we had
// to check for the existence of `firstEffect` to satisfy Flow. I think the
// only other reason this optimization exists is because it affects profiling.
// Reconsider whether this is necessary.
let subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
let rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
if (subtreeHasEffects || rootHasEffect) {
let prevTransition = ReactCurrentBatchConfig$3.transition;
ReactCurrentBatchConfig$3.transition = null;
let previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
let prevExecutionContext = executionContext;
executionContext |= CommitContext; // Reset this to null before calling lifecycles
ReactCurrentOwner$2.current = null; // The commit phase is broken into several sub-phases. We do a separate pass
// of the effect list for each phase: all mutation effects come before all
// layout effects, and so on.
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
let shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(root, finishedWork);
{
// Mark the current commit time to be shared by all Profilers in this
// batch. This enables them to be grouped later.
recordCommitTime();
}
commitMutationEffects(root, finishedWork, lanes);
resetAfterCommit(root.containerInfo); // The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork; // The next phase is the layout phase, where we call effects that read
{
markLayoutEffectsStarted(lanes);
}
commitLayoutEffects(finishedWork, root, lanes);
{
markLayoutEffectsStopped();
}
// opportunity to paint.
requestPaint();
executionContext = prevExecutionContext; // Reset the priority to the previous non-sync value.
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$3.transition = prevTransition;
} else {
// No effects.
root.current = finishedWork; // Measure these anyway so the flamegraph explicitly shows that there were
// no effects.
// TODO: Maybe there's a better way to report this.
{
recordCommitTime();
}
}
let rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
// This commit has passive effects. Stash a reference to them. But don't
// schedule a callback until after flushing layout work.
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
} else {
// There were no passive effects, so we can immediately release the cache
// pool for this render.
releaseRootPooledCache(root, remainingLanes);
{
nestedPassiveUpdateCount = 0;
rootWithPassiveNestedUpdates = null;
}
} // Read this again, since an effect might have updated it
remainingLanes = root.pendingLanes; // Check if there's remaining work on this root
// TODO: This is part of the `componentDidCatch` implementation. Its purpose
// is to detect whether something might have called setState inside
// `componentDidCatch`. The mechanism is known to be flawed because `setState`
// inside `componentDidCatch` is itself flawed — that's why we recommend
// `getDerivedStateFromError` instead. However, it could be improved by
// checking if remainingLanes includes Sync work, instead of whether there's
// any work remaining at all (which would also include stuff like Suspense
// retries or transitions). It's been like this for a while, though, so fixing
// it probably isn't that urgent.
if (remainingLanes === NoLanes) {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
legacyErrorBoundariesThatAlreadyFailed = null;
}
{
if (!rootDidHavePassiveEffects) {
commitDoubleInvokeEffectsInDEV(root.current, false);
}
}
onCommitRoot(finishedWork.stateNode, renderPriorityLevel);
{
if (isDevToolsPresent) {
root.memoizedUpdaters.clear();
}
}
{
onCommitRoot$1();
} // Always call this before exiting `commitRoot`, to ensure that any
// additional work on this root is scheduled.
ensureRootIsScheduled(root, now());
if (recoverableErrors !== null) {
// There were errors during this render, but recovered from them without
// needing to surface it to the UI. We log them here.
let onRecoverableError = root.onRecoverableError;
for (let i = 0; i < recoverableErrors.length; i++) {
let recoverableError = recoverableErrors[i];
let componentStack = recoverableError.stack;
let digest = recoverableError.digest;
onRecoverableError(recoverableError.value, {
componentStack: componentStack,
digest: digest
});
}
}
if (hasUncaughtError) {
hasUncaughtError = false;
let error$1 = firstUncaughtError;
firstUncaughtError = null;
throw error$1;
} // If the passive effects are the result of a discrete render, flush them
// synchronously at the end of the current task so that the result is
// immediately observable. Otherwise, we assume that they are not
// order-dependent and do not need to be observed by external systems, so we
// can wait until after paint.
// TODO: We can optimize this by not scheduling the callback earlier. Since we
// currently schedule the callback in multiple places, will wait until those
// are consolidated.
if (includesSomeLane(pendingPassiveEffectsLanes, SyncLane) && root.tag !== LegacyRoot) {
flushPassiveEffects();
} // Read this again, since a passive effect might have updated it
remainingLanes = root.pendingLanes;
if (includesSomeLane(remainingLanes, SyncLane)) {
{
markNestedUpdateScheduled();
} // Count the number of times the root synchronously re-renders without
// finishing. If there are too many, it indicates an infinite update loop.
if (root === rootWithNestedUpdates) {
nestedUpdateCount++;
} else {
nestedUpdateCount = 0;
rootWithNestedUpdates = root;
}
} else {
nestedUpdateCount = 0;
} // If layout work was scheduled, flush it now.
flushSyncCallbacks();
{
markCommitStopped();
}
return null;
}

Llegamos a la función donde se insertan los elementos en la página:

function appendChildToContainer(container, child) {
let parentNode;
if (container.nodeType === COMMENT_NODE) {
parentNode = container.parentNode;
parentNode.insertBefore(child, container);
} else {
parentNode = container;
parentNode.appendChild(child);
} // This container might be used for a portal.
// If something inside a portal is clicked, that click should bubble
// through the React tree. However, on Mobile Safari the click would
// never bubble through the *DOM* tree unless an ancestor with onclick
// event exists. So we wouldn't see it and dispatch it.
// This is why we ensure that non React root containers have inline onclick
// defined.
// https://github.com/facebook/react/issues/11918
let reactRootContainer = container._reactRootContainer;
if ((reactRootContainer === null || reactRootContainer === undefined) && parentNode.onclick === null) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(parentNode);
}
}

Como podemos ver, finalmente se añade el nodo al dom real.

Es así como se da por finalizado el ciclo de React, pero creo que no es necesario señalar que esto no es más que una introducción al funcionamiento interno de react, queda mucho más por investigar y descubrir.

Algoritmos que React usa

Priority queue

Una priority queue es una cola de prioridad, no se toma en cuenta el orden en el que los elementos se van agregando desde el punto de vista del interpreter del lenguaje, sino en base a la prioridad que le demos, la prioridad se puede obtener de manera dinámica, pero en un ejemplo más sencillo, podemos establecerle la prioridad en números enteros.

VER EJEMPLO:

https://replit.com/@alanjo/Fiber-Tree#index.js

Fiber Tree Traverse

Como vimos anteriormente, React usa las Fibras y crea un árbol a partir de ellas, siguiente esta estructura:

Empieza por el nodo raíz, y a partir de allí, su algoritmo es en realidad bastante simple, itera hasta el último hijo posible del nodo actual, una vez que se queda sin hijos para seguir iterando, sigue con el hermano de ese nodo si lo tuviese, así hasta que el nodo se queda sin nodo hijo y sin nodos hermanos para iterar, una vez realizado ese proceso, pasa al nodo padre,

Scheduler

El Scheduler no es un algoritmo propiamente dicho, pero si usa una priority queue y lógica interna para distribuir tareas, por lo tanto te lo voy a dejar incluido en el repositorio con ejemplos, y van a ir aumentando de dificultad en sus diferentes versiones, te recomiendo entender bien la priority queue antes de saltar al Scheduler.js en cualquiera de sus versiones.

Render

También te dejo un ejemplo sobre la lógica del render para que puedas entenderlo mejor, y te recomiendo que lo veas después de haber entendido la priority queue y el traverse del árbol de Fibras.

Commit

El commit es el proceso final de React, donde se insertan los elementos en el dom real, y se ejecutan los efectos secundarios, te recomiendo que lo veas después de haber entendido el traverse del árbol de Fibras y el render.

Ejemplos

Te recomiendo no reproducir el código del repositorio en entornos como replit o stackblitz, por un tema de que en sus versiones gratuitas la velocidad de computo suele ser bastante mala, con lo cual te recomiendo correrlo en el browser o en un entorno local de node.

Aqui tenés el repositorio con los algoritmos: ejemplos


Did you enjoy this post? Share it with your friends!