Cuando haces clic en un elemento, el evento no se dispara solo en ese elemento. Empieza en la parte superior del árbol y baja hasta el objetivo (la fase de captura), se dispara en el propio objetivo (la fase de objetivo) y luego vuelve a subir (la fase de burbujeo). Por defecto, los listeners que añades se ejecutan en la fase de burbujeo, por eso un clic en un hijo también activa los manejadores de sus ancestros. Añadir un listener en modo captura hace que se ejecute durante la bajada.
Llamar a stopPropagation dentro de un listener impide que el evento siga viajando, por lo que los listeners de las capas restantes no se ejecutan. No detiene a otros listeners del mismo elemento; de eso se encarga stopImmediatePropagation. Elige un punto de parada arriba para ver el orden de disparo cortarse.
Preguntas y respuestas frecuentes sobre este tema.
Son dos direcciones del mismo recorrido. La captura va de arriba abajo, desde el ancestro más externo hacia el objetivo, antes de alcanzarlo. El burbujeo va de abajo arriba, desde el objetivo hacia el ancestro más externo. Un listener añadido de la forma normal se ejecuta en la fase de burbujeo; pasar true como tercer argumento a addEventListener lo ejecuta en la fase de captura.
Por el burbujeo. Después de que el evento se dispara en el hijo (el objetivo), sube de nuevo por cada ancestro, ejecutando el listener de fase de burbujeo de cada uno en el camino. Esto suele ser útil: permite que un solo manejador en un contenedor responda a los clics de cualquier hijo, pero sorprende a quienes esperan que el evento se quede en el elemento que pulsaron.
Úsalo con moderación, solo cuando un hijo necesita manejar un evento y realmente debe evitar que un ancestro también reaccione, por ejemplo un botón dentro de una tarjeta clicable que no debe disparar el clic de la tarjeta. Recurrir a él con demasiada frecuencia suele indicar que el diseño del evento podría ser más simple; también puede romper otro código que legítimamente depende de que el evento llegue a un ancestro común.