DOM Events and Event Delegation
Key Learning Objectives
Master the 3 DOM event propagation phases: Capturing, Target, and Bubbling.
Understand the critical difference between event.target (originating element) and event.currentTarget (listener owner).
Implement high-performance event delegation using Element.prototype.closest() to handle dynamic elements with minimal memory footprint.
Distinguish between event.preventDefault(), event.stopPropagation(), and event.stopImmediatePropagation().
Clean up event listeners properly using AbortController signals to prevent browser memory leaks.
The Interview Problem
What is logged to the console when an event is dispatched to the child element, and how do capturing phases, currentTarget, and stopPropagation alter listener execution?
1class MockElement {2 constructor(name, parent = null) {3 this.name = name;4 this.parent = parent;5 this.listeners = [];6 }7 on(type, cb, useCapture = false) {8 this.listeners.push({ type, cb, capture: !!useCapture });9 }10 dispatch(type) {11 const logs = [];12 const path = [];13 let curr = this;14 while (curr) {15 path.unshift(curr);16 curr = curr.parent;17 }1819 const event = {20 target: this.name,21 currentTarget: null,22 stopped: false,23 stopPropagation() { this.stopped = true; }24 };2526 // Phase 1: Capturing (root -> target parent)27 for (let i = 0; i < path.length - 1 && !event.stopped; i++) {28 const el = path[i];29 event.currentTarget = el.name;30 el.listeners.filter(l => l.type === type && l.capture).forEach(l => {31 if (!event.stopped) l.cb(event, logs);32 });33 }3435 // Phase 2: Target36 if (!event.stopped) {37 event.currentTarget = this.name;38 this.listeners.filter(l => l.type === type).forEach(l => {39 if (!event.stopped) l.cb(event, logs);40 });41 }4243 // Phase 3: Bubbling (target parent -> root)44 for (let i = path.length - 2; i >= 0 && !event.stopped; i--) {45 const el = path[i];46 event.currentTarget = el.name;47 el.listeners.filter(l => l.type === type && !l.capture).forEach(l => {48 if (!event.stopped) l.cb(event, logs);49 });50 }5152 return logs;53 }54}5556const parent = new MockElement('parent');57const child = new MockElement('child', parent);5859parent.on('click', (e, logs) => logs.push(`parent:cap:${e.target}`), true);60parent.on('click', (e, logs) => logs.push(`parent:bub`), false);61child.on('click', (e, logs) => {62 logs.push(`child:${e.currentTarget}`);63 e.stopPropagation();64});6566const output = child.dispatch('click');67console.log(output.join(' '));
Predict Console Output
Select the option that matches what standard ECMAScript prints to the console:
parent:cap:child child:child
child:child parent:cap:child parent:bub
parent:cap:child child:child parent:bub
parent:cap:parent child:child
V8 Engine Execution Trace
Step 1 of 5 (Line 15)Event dispatch begins at the tree root. Propagation path is determined: [parent, child]. Phase 1 (Capturing) starts from the top.
Deep Technical Breakdown
The 3 Phases of DOM Event Propagation
When a user interacts with a DOM element (e.g., clicks a button), the browser browser engine creates an Event object and dispatches it through three distinct phases:
- Capturing Phase (Phase 1):
- The event travels down from
Window->Document-><html>-><body>down through ancestors to the target element's parent. - Listeners registered with
{ capture: true }oraddEventListener(type, fn, true)execute during this phase.
- The event travels down from
- Target Phase (Phase 2):
- The event reaches the registered
event.target. - Handlers registered directly on the target element execute in the order they were attached.
- The event reaches the registered
- Bubbling Phase (Phase 3):
- The event ascends from the target's parent back up the tree to
Window. - Standard listeners (
{ capture: false }, default) execute during this phase.
- The event ascends from the target's parent back up the tree to
target vs currentTarget
event.target: The actual deepest DOM node where the user initiated the event (e.g., the<span>or<button>clicked).event.currentTarget: The DOM node currently handling the event (whereaddEventListenerwas registered). In event delegation,currentTargetis the container, whiletargetis the clicked child item.
Event Delegation Pattern
Instead of binding 1,000 separate event listeners to every <li> inside a list, attach a single listener to the parent <ul>:
const list = document.querySelector('#item-list');
list.addEventListener('click', (event) => {
// Handle nested tags (e.g. <strong> inside <button>) using closest()
const deleteBtn = event.target.closest('.delete-btn');
if (!deleteBtn || !list.contains(deleteBtn)) return;
const itemId = deleteBtn.dataset.id;
deleteItem(itemId);
});stopPropagation() vs stopImmediatePropagation() vs preventDefault()
event.preventDefault(): Cancels default browser behavior (e.g., following a link, submitting a form). Does not stop event propagation.event.stopPropagation(): Stops the event from traveling further up (bubbling) or down (capturing) the DOM tree. Other listeners on the same element still execute.event.stopImmediatePropagation(): Halts propagation through the tree AND prevents any subsequent listeners on the same current element from executing.
Memory Leaks and Modern Cleanup with AbortSignal
const controller = new AbortController();
const { signal } = controller;
// Add listener with signal option
window.addEventListener('resize', handleResize, { signal });
// Cleanup everything in one call (e.g. on React unmount)
controller.abort();Common Traps & Mistakes
Confusing `event.target` with `event.currentTarget`, leading to bugs when clicking nested icons/spans inside buttons.
Assuming `event.preventDefault()` stops bubbling. It only suppresses native browser actions (form submissions, anchor navigations).
Failing to use `.closest('selector')` in event delegation, causing clicks on nested child elements to miss the intended target.
Attempting to remove anonymous function listeners via `removeEventListener('click', () => {})`, which creates a permanent memory leak.
FAANG Follow-Up Probes
Probe #1
What are passive event listeners (`{ passive: true }`), and why do they improve scroll performance on mobile browsers?
Probe #2
Which DOM events do NOT bubble by default (e.g., `focus`, `blur`, `mouseenter`, `mouseleave`) and how can delegation handle them?
Probe #3
How does React's SyntheticEvent system implement event delegation at the root container level in React 17/18?
