wandres.dev
CONSOLE II · Utilidades y contexto

Inspeccionar eventos: getEventListeners, monitorEvents y el panel

Cómo saber qué manejadores tiene un elemento, cuáles se disparan al interactuar, y de dónde vino cada uno, sin leer una línea del código de la aplicación.

⏱ 16 min

Los eventos son el sistema nervioso de una aplicación web y son casi completamente invisibles: no aparecen en el DOM, no dejan rastro en el marcado, y un manejador registrado desde JavaScript no se distingue de la ausencia de manejador mirando el elemento. Hay tres herramientas que los hacen visibles, y entre las tres responden a las tres preguntas que importan: qué hay registrado, qué se está disparando, y quién lo registró.

🎯 Al terminar esta lección sabrás
  • Listar los manejadores registrados en un elemento y en sus ancestros.
  • Monitorizar los eventos que emite un elemento durante una interacción real.
  • Localizar en el código el sitio donde se registró un manejador concreto.
  • Diagnosticar manejadores duplicados, propagación detenida y eventos que no llegan.

getEventListeners

Devuelve un objeto cuyas claves son tipos de evento y cuyos valores son arrays de descriptores de los manejadores registrados en ese elemento.

getEventListeners($0);

Cada descriptor contiene la función manejadora, si usa captura, si es pasivo, y si es de un solo uso. La función es un valor real, así que se puede inspeccionar, y ahí está el truco que resuelve la pregunta de dónde se registró.

// Salta al codigo fuente del manejador de clic de este elemento
inspect(getEventListeners($0).click[0].listener);

inspect sobre una función abre el panel de Sources en su definición, con el source map aplicado si lo hay. Eso lleva del “algo pasa al hacer clic” al fichero y la línea exactos en dos expresiones.

Hay un límite importante: getEventListeners solo devuelve los registrados en ese nodo concreto. Con delegación de eventos —el patrón dominante en los frameworks, donde un único manejador en la raíz gestiona toda la aplicación— el elemento que te interesa no tendrá nada y el manejador estará en document o en el contenedor de montaje.

La forma de recorrer la cadena.

// Manejadores en el elemento y en toda su cadena de ancestros
function listenersDeLaCadena(el) {
  const filas = [];
  for (let n = el; n; n = n.parentElement || (n === document.documentElement ? document : null)) {
    const l = getEventListeners(n);
    for (const tipo of Object.keys(l)) {
      filas.push({ nodo: n.nodeName, tipo, cuantos: l[tipo].length });
    }
    if (n === document) break;
  }
  console.table(filas);
}
listenersDeLaCadena($0);
💡
Tip

El panel de Elements tiene una pestaña lateral de manejadores de eventos que muestra lo mismo con interfaz, y con dos casillas muy útiles: una para incluir los de los ancestros y otra para filtrar los de frameworks. Es más cómoda para explorar; la consola es mejor para consultar programáticamente.

monitorEvents

Registra un manejador temporal que escribe en la consola cada evento del tipo indicado que reciba el elemento.

monitorEvents($0, 'click');
monitorEvents($0, ['focus', 'blur', 'input']);
monitorEvents(window, 'resize');
unmonitorEvents($0);          // quita todos los del elemento
unmonitorEvents($0, 'click'); // quita solo ese tipo

Acepta además cuatro categorías que agrupan tipos relacionados y que ahorran escribir listas: mouse, key, touch y control. La última incluye los eventos de formulario: resize, scroll, zoom, focus, blur, select, change, submit, reset.

monitorEvents($0, 'control');   // todo lo relacionado con formularios
monitorEvents(document.body, 'key');

El objeto que se imprime es el evento completo, expandible, con su tipo, su objetivo, sus coordenadas, sus modificadores y su ruta de propagación. Ese último dato, composedPath, es especialmente valioso porque revela el recorrido completo del evento incluidas las fronteras de shadow DOM.

Los tres diagnósticos que resuelve casi solo.

El evento que no llega. Monitorizas el elemento, interactúas, y no aparece nada. Eso descarta toda la lógica de la aplicación: el evento ni siquiera se está emitiendo sobre ese elemento. Las causas: hay otro elemento encima capturando el clic, el elemento tiene pointer-events: none, o está desactivado.

El evento que llega dos veces. Aparecen dos líneas por interacción. Manejador registrado dos veces, o un evento sintético del framework sumado al nativo.

El evento que llega con el objetivo equivocado. El campo de objetivo del evento apunta a un hijo, y tu lógica esperaba el contenedor. Es el bug clásico de delegación mal escrita, y se ve de un vistazo.

Cazar quién registra los manejadores

Cuando lo que quieres saber no es qué hay sino quién lo puso, la técnica es envolver el registro.

// Traza cada registro de manejador con la pila de quien lo hizo
const registrar = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (tipo, fn, opciones) {
  if (tipo === 'click') {          // filtra para no ahogarte
    console.groupCollapsed('addEventListener', tipo, this);
    console.trace();
    console.groupEnd();
  }
  return registrar.call(this, tipo, fn, opciones);
};

Poner eso antes de que cargue la aplicación es lo que lo hace útil, y para eso el sitio correcto es un breakpoint en la primera sentencia del primer script, o un snippet ejecutado con la página en pausa. Con el filtro por tipo, la salida es manejable.

Propagación, captura y por qué tu manejador no se ejecuta

El último grupo de diagnósticos. Un evento recorre tres fases: baja desde la raíz hasta el objetivo en captura, se dispara en el objetivo, y sube de vuelta en burbujeo. Un manejador registrado con capture se ejecuta en la bajada; sin él, en la subida.

Los cuatro fallos frecuentes y cómo se ven.

stopPropagation en un ancestro durante la captura impide que el evento llegue siquiera al objetivo. Con monitorEvents en el objetivo no ves nada; con monitorEvents en un ancestro sí. Esa asimetría es el diagnóstico.

stopImmediatePropagation en el mismo elemento impide que se ejecuten los manejadores registrados después. Aparece el evento pero tu lógica no corre.

Eventos que no burbujean. focus, blur, load, error, mouseenter y mouseleave no suben. Los equivalentes que sí lo hacen son focusin, focusout, mouseover y mouseout. Intentar delegar focus desde un contenedor no funciona nunca y es un error muy común.

Manejadores pasivos. Un manejador registrado como pasivo no puede llamar a preventDefault, y el navegador lo ignora si lo intenta, con un aviso en consola. Los de touchstart, touchmove y wheel son pasivos por defecto en muchos casos, lo que rompe la lógica de bloqueo de scroll escrita antes de ese cambio.

El evento sintético del framework no es el evento del navegador

Hay una capa que todas las herramientas de esta lección atraviesan sin avisar y que explica una categoría entera de confusiones: varios frameworks no registran manejadores donde tú escribes que los registras. Cuando escribes un manejador de clic en un componente, es muy posible que no haya ningún manejador en ese nodo del DOM: el framework registra uno solo en la raíz de la aplicación, escucha ahí todos los clics, y cuando llega uno reconstruye qué componente debería recibirlo y llama a tu función con un objeto de evento propio, no con el del navegador. Las consecuencias son cuatro y todas confunden. Primera: getEventListeners sobre tu elemento devuelve un objeto vacío, y la conclusión de que “no hay manejador” es falsa. Segunda: monitorEvents sobre el elemento sí ve el evento nativo, porque el nativo sí llega, pero lo que ves no es lo que tu componente recibe. Tercera: llamar a stopPropagation en el evento sintético no detiene el nativo, así que un manejador registrado a mano en un ancestro se ejecuta igual, y ese desajuste produce bugs desconcertantes al mezclar código de framework con código propio. Y cuarta, la más importante para depurar: un breakpoint de evento de la categoría de ratón se detiene en el código del framework, no en tu componente, porque el manejador nativo es suyo; tu función se llamará varios marcos más abajo. La forma de trabajar con esto es asumirlo: cuando quieras saber si tu manejador de componente corre, el sitio de poner el breakpoint es tu función, no el evento; y cuando quieras saber si el evento nativo llega al DOM, entonces sí es el breakpoint de evento. Distinguir esas dos preguntas antes de elegir la herramienta ahorra la mitad del tiempo perdido depurando interacción en aplicaciones modernas.