Hamburger Icon
Las bases de los eventos en JavaScript

Las bases de los eventos en JavaScript

Para entender JavaScript es necesario entender cómo funcionan los eventos ya que, por diseño, este es un lenguaje orientado a eventos.

También es bueno conocer cómo JavaScript se ejecuta en el navegador y, para eso, puedes consultar el post Destripando JavaScipt - Parte 2.

Otra parte básica es el DOM. Puedes ver qué puedes hacer con esta API en esta Introducción al DOM de JavaScript.

Arquitectura publicador/suscriptor

Una vez las bases están más o menos claras, será más sencillo entender que JavaScript se compone de elementos publicadores de eventos y de otros suscriptores de los mismos.

Como quizá te imagines, los publicadores no son más que código que emite eventos, mientras que los suscriptores son código que captura eventos.

Captura de eventos

JavaScript pone a nuestra disposición el método addEventListener para capturar elementos. Este método espera el nombre del evento que se quiere capturar y la función que hay que llamar cuando se dispare dicho evento.

Por ejemplo, para capturar el clic en un botón haremos algo como esto:

const boton = document.querySelector("button")

boton.addEventListener("click", () => {
  console.log("clic en botón")
})

De este modo, cuando el usuario haga clic en el botón, se llamará a la función que hemos indicado. También es posible disparar eventos de forma programática:

const boton = document.querySelector("button")

boton.click()

Ten en cuenta también que el evento se puede disparar un número indeterminado de veces. Además, aunque no capturemos el evento, igualmente se dispara.

Los eventos tienen un comportamiento por defecto. Si has trabajado con formularios sabrás que, por ejemplo, el botón submit recarga o redirige la página, o cuando haces clic en un checkbox se marca o desmarca.

Propagación de eventos

Cuando un evento se dispara, lo más normal es pensar que se dispara solo en el elemento afectado, pero no. Imagina este código HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Ejemplo</title>
  </head>
  <body>
    <button>Haz clic</button>
  </body>
</html>

Cuando se hace clic en el botón, efectivamente se dispara el evento clic en el mismo, pero también en sus ancestros. Si no me crees, captura el evento y verás:

const button = document.querySelector("button")
const html = document.documentElement
const body = document.body

window.addEventListener("click", () => {
  console.log("Clic en la ventana")
})
html.addEventListener("click", () => {
  console.log("Clic en tag html")
})
body.addEventListener("click", () => {
  console.log("Clic en tag body")
})
button.addEventListener("click", () => {
  console.log("Clic en botón")
})

// Después de hacer clic en el botón, por consola se imprime esto:
/* 
Clic en botón 
Clic en tag body 
Clic en tag html 
Clic en la ventana 
*/

El resultado siempre será en el mismo orden, ya que primero se dispara el clic en el botón, luego en el ancestro inmediatamente superior y así sucesivamente hasta llegar a la ventana.

Esto sucede porque aunque estés haciendo clic en el botón, este forma parte a su mismo tiempo de una estructura HTML. Al hacer clic en el botón, también estás haciendo clic en el cuerpo, la raíz del documento y en la ventana.

Por lo tanto, el orden en el que definas la captura de eventos no influye en absoluto en el momento en que se gestionará el disparo, a no ser que definas varias capturas de evento en el mismo elemento. En este último caso sí que importa el orden:

button.addEventListener("click", () => {
  console.log("Primer clic")
})
button.addEventListener("click", () => {
  console.log("Segundo clic")
})

Fases de los eventos

Como indicaba antes, la propagación de un evento es desde el nodo afectado hasta la raíz del documento. Esto nos puede hacer pensar que los eventos empiezan en el nodo que los dispara, pero no, eso no es del todo cierto.

Los eventos pasan por tres fases, en este orden:

  • Captura: el evento empieza en la raíz del DOM y se propaga hacia abajo hasta el objetivo.
  • Buscando el objetivo: es cuando el evento alcanza el nodo que lo ha disparado.
  • Bubbling: el evento se propaga hacia arriba hasta la raíz del DOM, a la inversa de la captura.

Todos los eventos pasan por las dos primeras fases, captura y búsqueda del objetivo, pero no todos los eventos llegan a la fase de burbujeo o bubbling.

Primero: captura

Todos los eventos empiezan arriba, en la raíz del DOM, y se van propagando hacia abajo hasta el nodo que los ha disparado. Durante ese proceso, el evento se dispara en cada nodo por el que va pasando.

JavaScript nos da un mecanismo para detectar, a partir de un nodo, cuándo un evento se ha disparado en un nodo hijo:

button.addEventListener(
  "clic",
  () => {
    console.log("clic en hijo")
  },
  { captura: true }
)

Con el parámetro capture en true, el evento se dispara cuando se hace clic en uno de los nodos hijos del botón. Este mecanismo puede ser útil para capturar eventos que no pasan por la fase de bubbling.

Segundo: objetivo

En algún momento, durante la propagación hacia abajo en la estructura del DOM, se alcanza el nodo que ha disparado el evento. Ese es el nodo objetivo.

En la función que usamos para gestionar el disparo del evento, se puede, opcionalmente, recibir como parámetro el evento mismo, a través del cual podemos acceder al nodo objetivo:

button.addEventListener("clic", (event) => {
  console.log(`Objetivo: ${event.target}`)
})

// con event.target podemos acceder al nodo objetivo del evento

El nodo objetivo siempre será el mismo independientemente de la fase en la que se encuentre el evento. Para interactuar con el nodo actual según la fase en la que se encuentre el evento, podemos usar event.currentTarget.

Tercero: bubbling / ¿burbujeo?

En español me suena muy raro, así que me quedo con bubbling. En esta fase el evento se propaga desde el nodo objetivo hacia arriba hasta la raíz del DOM, pasando por todos los nodos ancestros.

Algunos eventos no llegan a esta fase. Con el parámetro event de la función que lo gestiona podemos acceder a event.bubbles, que es un booleano que nos indica si el evento llega a esta fase o no.

El evento scroll, por ejemplo, no llega a esta fase, ya que ejecutar el evento scroll en los ancestros de un contenedor acabaría generando efectos no deseados.

Parar la propagación de eventos

En algunos casos querremos evitar el comportamiento por defecto de un evento. Puede ser que queramos evitar la propagación de un evento, tanto si la misma va hacia abajo (buscando el objetivo) o hacia arriba (volviendo a la raíz del DOM).

Esto lo podemos conseguir con el método event.stopPropagation(). Siguiendo el mismo ejemplo que al principio:

const button = document.querySelector("button")
const html = document.documentElement
const body = document.body

window.addEventListener(
  "click",
  (event) => {
    console.log(`Clic en la ventana en fase ${event.eventPhase}`)
    event.stopPropagation()
  },
  { capture: true }
)
html.addEventListener("click", () => {
  console.log("Clic en tag html")
})
body.addEventListener("click", () => {
  console.log("Clic en tag body")
})
button.addEventListener("click", () => {
  console.log("Clic en botón")
})

// Después de hacer clic en el botón, por consola se imprime esto:
/* 
Clic en la ventana en fase 1 
*/

El evento, en su fase de captura del objetivo (el botón), detiene la propagación y no llega hasta nodos inferiores.

Esto sucede tanto si haces clic en el botón como si haces clic en cualquier punto de la ventana, ya que el evento se captura en cualquier caso.

Sobreescribir el comportamiento de eventos

Antes he indicado que en un formulario, el botón submit recarga o redirige la página. Quizás no queramos ese comportamiento y queramos sobreescribirlo por otro diferente.

Para hacer algo así, tenemos disponible el método event.preventDefault():

const form = document.querySelector("form")

form.addEventListener("submit", (event) => {
  event.preventDefault()
  // aquí tu implementación para el envío del formulario
  // ...
})

Conclusión

Si entiendes las tres fases por las que pasan los eventos, ya lo tienes. No hay muchos más detalles más.

Cuando un evento se dispara, se busca el objetivo que lo ha disparado empezando por la raíz del DOM bajando hacia abajo en la estructura, hasta que lo encuentra.

Luego, vuelva hacia arriba donde ha empezado, ejecutando el evento en todos y cada uno de los nodos por los que ha pasado.

Si entiendes esto y aprendes a controlar y gestionar la ejecución de los eventos cuando te interese, puedes hacer lo que quieras con la web.

Si te interesa el tema y quieres profundizar más, tienes el artículo de Aleksandr Hovhannisyan donde está muy bien exlicado y con más ejemplos y casos.