Breakpoints de evento: parar en la interacción, no en el código
Las categorías del árbol de eventos, los breakpoints de instrumentación que no son eventos de usuario, y seis casos reales resueltos sin buscar código.
Un breakpoint de evento detiene la ejecución en el primer manejador que se ejecute para el tipo de evento que elijas, en cualquier parte de la página, sin que tengas que saber quién lo registró ni dónde está. Es la herramienta que responde a “algo pasa cuando pulso aquí y no sé qué”, que es una de las tres o cuatro preguntas más frecuentes de la depuración web. Y su categoría más útil no son los eventos de usuario: son los de instrumentación, que permiten tomar el control del navegador en momentos donde no hay ningún evento que escuchar.
- Activar breakpoints de evento por categoría y por tipo concreto.
- Usar los breakpoints de instrumentación para tomar el control antes de que corra tu código.
- Resolver los seis casos canónicos de esta familia sin leer el código de la aplicación.
- Reconocer y evitar el ruido que producen las categorías demasiado amplias.
El árbol de categorías
En el subpanel de breakpoints de evento hay un árbol de casillas agrupadas por categoría. Marcar la categoría entera activa todos sus tipos; marcar un tipo concreto activa solo ese.
Las categorías que se usan de verdad, y para qué sirve cada una.
Ratón. click, dblclick, mousedown, mouseup, mouseover, mouseout, contextmenu, wheel. La categoría estrella para diagnosticar interacción.
Teclado. keydown, keyup, keypress. Para atajos que hacen algo inesperado.
Control. focus, blur, change, input, submit, reset, select, resize, scroll. Es la categoría de formularios y la que resuelve la mayoría de los bugs de validación.
Carga. load, beforeunload, unload, error, abort, DOMContentLoaded, hashchange, popstate. El ciclo de vida del documento.
Temporizador. setTimeout, clearTimeout, setTimeout fired, y los equivalentes de setInterval. Fíjate en la distinción entre el registro del temporizador y su disparo: son dos breakpoints distintos y resuelven preguntas distintas.
Animación. requestAnimationFrame, cancelAnimationFrame, requestAnimationFrame fired, con la misma distinción.
Script. La primera sentencia de cada script, y los bloqueos por política de seguridad de contenido.
Puntero, táctil, portapapeles, arrastre, medios, notificaciones y trabajadores, cada una con sus tipos.
El campo de búsqueda del subpanel filtra el árbol por nombre de evento. Con más de cien tipos repartidos en veinte categorías, escribir scroll o paste es mucho más rápido que desplegar.
Los breakpoints de instrumentación
Dentro del árbol hay entradas que no corresponden a eventos del DOM sino a momentos internos del navegador. Son los más potentes y los menos conocidos.
Primera sentencia de script. Detiene la ejecución en la primera línea de cada script que se cargue. Es la única forma de tomar el control de una página antes de que su código haga nada, lo cual permite instrumentar el arranque: envolver métodos, poner breakpoints en ficheros que aún no existían, sustituir globales. Es ruidoso —se detiene una vez por script— y por eso se usa activándolo, recargando, haciendo lo que haga falta en el primer script, y desactivándolo.
Disparo de temporizador. Detiene la ejecución cuando un setTimeout o un setInterval ejecuta su callback. Con la pila incluida, dice quién lo programó. Resuelve el clásico “algo pasa un segundo después de cargar y no sé qué”.
Registro de temporizador. Detiene cuando alguien crea un temporizador. Combinado con el anterior, permite reconstruir toda la actividad temporizada de una página.
Disparo de fotograma. El equivalente para requestAnimationFrame, que es donde vive la lógica de animación en JavaScript.
Bloqueo por política de seguridad. Detiene cuando la política de seguridad de contenido impide ejecutar algo. Es la forma de saber exactamente qué script se intentó cargar y desde dónde.
Seis casos reales
Caso uno: el modal que se cierra solo. Abres un diálogo y se cierra inmediatamente. Activa el breakpoint de click y vuelve a abrirlo. Te detendrás dos veces: una en el manejador del botón que abre, y otra en el manejador que cierra. La segunda pila te dice quién es. La causa habitual es un manejador de clic en el documento que cierra al pulsar fuera, y que se registra antes de que el evento de apertura termine de propagarse, con lo que el propio clic de apertura lo cierra.
Caso dos: el formulario que se envía cuando no debería. Activa submit. Si te detienes, el envío es real y la pila dice quién lo provocó; a menudo es la tecla de entrada en un campo de texto dentro de un form, que envía por comportamiento nativo. Si no te detienes, no hay envío y lo que ves es otra cosa.
Caso tres: el foco que se escapa. Un campo pierde el foco al escribir y no sabes por qué. Activa blur y focus en la categoría de control. La pila del blur dice qué código movió el foco, que casi siempre es un render que sustituyó el nodo del DOM por otro nuevo.
Caso cuatro: la página que salta al cargar. Activa scroll y recarga. La primera pila que aparezca dice quién movió el scroll, que suele ser un scrollIntoView de una librería de anclas o de un componente que quiere ser visible.
Caso cinco: la petición que se dispara al segundo. Activa el disparo de temporizador. Cuando se detenga, la pila muestra el código del callback, y si la pila asíncrona está activa, muestra también quién programó el temporizador, que es lo que de verdad quieres saber.
Caso seis: el script de terceros que no sabes de dónde sale. Activa la primera sentencia de script y recarga. Cada parada te enseña un script; el que no reconozcas es el intruso, y la pila dice quién lo insertó.
Controlar el ruido
El problema práctico de esta familia es que categorías amplias producen paradas constantes. Tres tácticas.
Elige el tipo, no la categoría. Marcar la categoría de ratón entera hace que te detengas en cada movimiento del cursor. Marcar solo click es manejable.
Usa mousedown en vez de click cuando investigues secuencias. El orden real es mousedown, mouseup, click, y a veces el código relevante está en el primero.
Combina con la lista de ignorados. Sin ella, cada clic te detiene en el manejador delegado del framework y hay que subir la pila cada vez. Con ella, te detiene directamente en el primer marco de tu código.
Y una cuarta que resuelve el caso difícil: cuando lo que quieres es contar cuántas veces se dispara un evento sin detenerte, monitorEvents de la consola es mejor herramienta que el breakpoint. Los dos son complementarios: monitorizar para entender la secuencia, breakpoint para inspeccionar el estado en una de ellas.
Hay una fuente de desconcierto que conviene conocer antes de usar mucho esta familia, y que hace que muchos desarrolladores concluyan erróneamente que los breakpoints de evento “no funcionan con React”. Funcionan perfectamente; lo que ocurre es que te detienen donde el navegador ejecuta el manejador, y varios frameworks no registran manejadores en el elemento donde tú escribes el onClick. Registran uno solo en la raíz de la aplicación, escuchan ahí todos los eventos, y cuando llega uno reconstruyen internamente qué componente debía recibirlo y llaman a tu función. Eso significa tres cosas que hay que tener claras para no perder tiempo. Primera: el breakpoint se detiene en el código del framework, y tu componente aparece varios marcos más abajo en la pila, o incluso no aparece todavía porque el framework aún no ha decidido a quién llamar; si continúas la ejecución, en algún momento llegará a tu función. Segunda: la lista de ignorados es lo que hace esta técnica usable con frameworks, porque hace que el depurador salte todos esos marcos y te presente directamente el primer código tuyo; sin ella la experiencia es mala y con ella es excelente, y esa es la diferencia entre quien dice que esto no sirve y quien lo usa a diario. Y tercera, que es la que resuelve el caso general: cuando quieras parar en tu manejador y no en el del framework, el breakpoint correcto no es el de evento sino uno de línea en tu función, y la forma rápida de llegar a ella sin buscarla es hacerlo una vez con el breakpoint de evento, mirar la pila, seleccionar tu marco y poner ahí el breakpoint de línea definitivo. El breakpoint de evento es el instrumento de descubrimiento; el de línea es el de trabajo. Usarlos en ese orden es lo que convierte quince minutos de búsqueda en quince segundos.