23. The event model: capturing, bubbling, delegation, passive listeners
DOM events travel through three phases: capturing (root → target), target, then bubbling (target → root) — this two-directional travel is what lets a listener on an ancestor react to events from descendants it doesn't even know exist yet.
// listener fires during the BUBBLING phase by default (3rd arg false/omitted)
document.body.addEventListener("click", (e) => console.log("bubble: body"));
// listener fires during the CAPTURING phase (3rd arg true)
document.body.addEventListener("click", (e) => console.log("capture: body"), true);
// clicking a deeply nested button logs:
// "capture: body" (travels down first)
// ... then any listeners on the target itself ...
// "bubble: body" (travels back up)
Event delegation exploits bubbling to attach ONE listener to a container instead of one per child — critical for lists with thousands of items or items added dynamically after the listener was set up:
// One listener handles clicks on ANY current or future <li>, no per-item binding needed
document.querySelector("ul").addEventListener("click", (e) => {
const li = e.target.closest("li");
if (li) console.log("clicked:", li.textContent);
});
Passive listeners tell the browser "this listener will never call
preventDefault()", letting the browser start scrolling immediately instead
of waiting to see if your touchstart/wheel handler cancels it — a
measurable, real scroll-jank fix on mobile:
el.addEventListener("touchstart", handleTouch, { passive: true });