DOM Events and Event Delegation

Medium100% Free~25 mins#dom#events#event-delegation#bubbling#capturing#target#current-target#browser-apis
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 }
18
19 const event = {
20 target: this.name,
21 currentTarget: null,
22 stopped: false,
23 stopPropagation() { this.stopped = true; }
24 };
25
26 // 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 }
34
35 // Phase 2: Target
36 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 }
42
43 // 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 }
51
52 return logs;
53 }
54}
55
56const parent = new MockElement('parent');
57const child = new MockElement('child', parent);
58
59parent.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});
65
66const output = child.dispatch('click');
67console.log(output.join(' '));
Predict Console Output
Interactive Challenge

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.

Call Stack (Top = Active)
Global Execution Context
child.dispatch('click')
Lexical Scope / Bindings
event.target:'child'
propagationPhase:Capturing (Phase 1)
Console Stream
> [empty]

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:

  1. 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 } or addEventListener(type, fn, true) execute during this phase.
  2. 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.
  3. 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.

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 (where addEventListener was registered). In event delegation, currentTarget is the container, while target is 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>:

javascript
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

javascript
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?