Frontend engineer path · The browser · Topic 8 of 17

The DOM and its events

Not started

The DOM is the page as a tree of objects your script can read and change. When something happens, such as a click, a key press or a form submit, the browser creates an event object and sends it along a path through that tree: down from the window to the target, then back up. Any listener on that path can react, which is what makes event delegation possible.

Why it matters: Every click handler you write runs inside this model. It explains why a listener on a list fires for its buttons, and why some events never reach the parent.

  1. 1The DOM is a live tree

    Each tag becomes a node object. Changing a node changes the page, and the browser picks the change up at its next rendering step.

  2. 2Three phases

    An event travels down from the window to its target (capture), fires on the target, then travels back up through every ancestor (bubble).

  3. 3target vs currentTarget

    event.target is the element the event happened on; event.currentTarget is the element whose listener is running now.

  4. 4Delegate

    One listener on a container can handle events from all of its children, including ones added later, by checking event.target.

1

How it works

The explanation, with examples you can run.

The route of one event

The path is fixed when the event is dispatched: the window, the document, and every ancestor of the target. The event then runs in three phases:

  1. Capture: from the window down to the target's parent. Only listeners added with { capture: true } run here.
  2. Target: listeners on the target itself.
  3. Bubble: from the target's parent back up to the window. Ordinary listeners run here.

Not every event bubbles. focus, blur, mouseenter and mouseleave don't, while focusin, focusout, click, input and keydown do. stopPropagation() ends the journey after the current element; preventDefault() is separate and only cancels the browser's default action, such as following a link or submitting a form.

Delegation

Instead of one listener per button, put one on the container and find out which child was hit. It uses less memory, and it keeps working for rows added after the listener was attached. This runs in the Try it frame, which has its own document:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

closest() walks up from the element that was actually hit (here the <span>) to the nearest matching ancestor, so it doesn't matter how deep the markup inside the button goes.

Listeners and the main thread

Listeners run on the main thread as tasks of the event loop, so a slow handler delays the next frame and every other event. For touchstart, touchmove and wheel, pass { passive: true } when you never call preventDefault(): the browser can then scroll straight away instead of waiting for your code. Remove listeners you no longer need, or pass an AbortSignal with { signal } and abort it to remove a group at once.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A click listener is on a <ul>. The user clicks a <span> inside a <button> inside an <li>. Inside the listener, what are event.target and event.currentTarget?
  2. 2.Delegation works for click. Why does a focus listener on a form never fire when its inputs get focus?
  3. 3.What does this print when the button is clicked?
    outer.addEventListener("click", () => console.log("outer"))button.addEventListener("click", (e) => {  console.log("button")  e.stopPropagation()})outer.addEventListener("click", () => console.log("outer capture"), { capture: true })

0 of 3 right so far. Get all three to finish this topic.