JS: API del DOM

Teoría: Interceptación y propagación

Imaginemos que tenemos varios elementos, cada uno con un manejador de eventos click. Uno de los elementos es externo, un <div>. Dentro de él hay dos elementos con botones:

<div>
  <button id="send">Send</button>
  <button id="cancel">Cancel</button>
</div>

Si haces clic en un área de un elemento externo, se ejecutará el controlador asociado a ese elemento externo.

Si haces clic en un elemento interno, también se registrará un clic en el elemento externo automáticamente. Esto significa que se activarán ambos eventos.

Entonces, surge una pregunta lógica: «¿En qué orden se ejecutan estos eventos después de hacer clic en el botón?». Por lo general, el evento viaja a través del árbol desde la raíz hasta el elemento más profundo donde se activó, y luego regresa en dirección inversa. Este recorrido de ida y vuelta del evento se llama sus etapas o fases, y a continuación hablaremos de ellas

Captura (Capturing)

Cuando el evento acaba de ocurrir, comienza a moverse a través del árbol DOM desde el nodo raíz hasta el más profundo en el que ocurrió el evento:

| |
---------------| |---------------
| div          | |              |
|   -----------| |-----------   |
|   | button   \ /          |   |
|   -------------------------   |
|        Captura del Evento     |
---------------------------------

Durante la etapa de captura, se ejecutarán los controladores que estén vinculados a esta etapa. La vinculación se regula mediante el tercer parámetro de la función addEventListener:

div.addEventListener('click', () => alert('Div alert'), true);
button1.addEventListener('click', () => alert('Button Send alert'), true);
button2.addEventListener('click', () => alert('Button Cancel alert'), true);

En el ejemplo anterior, el usuario hizo clic en el botón "Send", que está dentro de un elemento div. El valor true asocia los controladores a la fase de captura. El evento se propaga hacia abajo a través del árbol desde el nodo raíz. El nodo raíz es el div, y el controlador de este evento se activa en ese elemento. Luego, el evento se mueve al elemento hijo donde ocurrió el evento, y se activa el controlador de evento de ese elemento. En nuestro caso, es el botón "Send". El resultado sería el siguiente:

Div alert
Button Send alert

Ten en cuenta que el controlador del botón "Cancel" no se activó, ya que el evento no ocurrió en ese botón.

const div = document.querySelector('div');
const button1 = document.querySelector('#send');
const button2 = document.querySelector('#cancel');
div.addEventListener('click', () => alert('Div alert'), true);
button1.addEventListener('click', () => alert('Button Send alert'), true);
button2.addEventListener('click', () => alert('Button Cancel alert'), true);

Codepen

Bubbling (Vuelta)

Después de detenerse en el elemento target, comienza el proceso de burbujeo:

/ \
---------------| |---------------
| div          | |              |
|   -----------| |-----------   |
|   | button   | |          |   |
|   -------------------------   |
|        Bubbling del Evento    |
---------------------------------

Esta es la etapa que se asume cuando se llama a addEventListener sin el tercer parámetro:

div.addEventListener('click', () => alert('Div alert'));
button1.addEventListener('click', () => alert('Button Send alert'));
button2.addEventListener('click', () => alert('Button Cancel alert'));

En esta etapa, los controladores se ejecutan de adentro hacia afuera:

Div alert
Button Send alert

Codepen

La propagación de eventos es una parte crucial del comportamiento del DOM. Sin ella, sería imposible implementar eventos que se activan en bloques completos en lugar de solo en los elementos más profundos. Un ejemplo simple es un menú contextual.

Otro ejemplo son las tablas diseñadas al estilo de Excel. Estas tablas pueden ser enormes. Agregar eventos a cada celda resultaría en la creación de muchos manejadores idénticos que deben añadirse continuamente a medida que la tabla crece. Además del código adicional, este enfoque también ralentiza significativamente el rendimiento con grandes volúmenes de datos. Es mucho más eficiente colocar un solo manejador en toda la tabla. En el siguiente ejemplo, se añade un manejador de clics a la tabla, de modo que se activará al hacer clic en cualquier botón dentro de la tabla.

const table = document.querySelector('table');

table.addEventListener('click', () => alert('Table clicked!'))

Codepen

Modelo W3C

La mayoría de los eventos pasan por ambas etapas, primero se sumergen en el árbol y luego suben hasta la parte superior. La etapa de captura se usa raramente, la mayoría de los controladores se agregan a la etapa de burbujeo.

En la lección anterior, conocimos el objeto e.target. Este es el elemento más profundo al que se está sumergiendo. Durante el proceso de burbujeo, target no cambia. Gracias a él, siempre se puede saber dónde ocurrió exactamente el evento.

Además de target, está disponible el objeto currentTarget, que es el elemento al que se adjunta este controlador. Dependiendo de la situación, se utiliza uno u otro:

Event Stages

En una situación normal, el evento debe burbujear hasta el final, pero a veces pueden surgir situaciones en las que el burbujeo no sea deseado.

Se puede detener de dos formas:

  • event.stopPropagation() - detiene la propagación, pero permite que todos los controladores en el elemento actual se ejecuten.
  • event.stopImmediatePropagation() - detiene la propagación junto con todos los controladores.

En resumen, la captura es un mecanismo de transferencia de eventos a un controlador. Durante la captura o el burbujeo, el evento se transfiere de un elemento a otro. Si un elemento tiene un controlador para ese evento, se activa, lo que se llama capturar el evento con el controlador. Usando los métodos descritos anteriormente, podemos detener el proceso de transferencia de eventos entre elementos.