Browser events: bubbling, capturing, and delegation

Meteor Software
Meteor Blog
Published in
6 min readSep 6, 2013

--

By David Greenspan

Legend has it that back in the day, Netscape Navigator and Internet Explorer had different, incompatible ways of propagating events to multiple handlers; Netscape “captured” while Internet Explorer “bubbled.” So the W3C decided to standardize both behaviors when they createdaddEventListener, which can be configured to use either model. Let's take a look at how these models play into the "event delegation" model used by modern web frameworks.

Suppose you have this HTML structure:

<body> <p> <a><span>Hello</span></a> </p> </body>

If we add a “click” event listener to the A, we’d expect it to fire when the user clicks the SPAN. We’d also expect clicking on the SPAN to trigger listeners on the P and BODY. If we added listeners to every element — the SPAN, the A, the P, and the BODY — we’d expect them to all fire when the user clicks the SPAN.

The “bubbling” model achieves this by saying the event bubbles from bottom to top, visiting each handler in turn. First it visits the SPAN’s handler, and if that handler doesn’t cancel the event, it propagates up to the A, and so on. The “capturing” model says instead that event handlers are visited top-to-bottom. The BODY handler captures the event first, and if it doesn’t cancel the event, it propagates downwards to the P, and so on.

The W3C model combines capturing and bubbling by saying that an event first makes its way downwards from the BODY to the SPAN visiting the “capturers,” and then (assuming propagation isn’t stopped) bubbles back up visiting the non-capturing handlers. Using addEventListener, you can choose whether each listener participates in the capturing phase or the bubbling phase. Unfortunately, you don't get this choice in IE 6/7/8, which don't support addEventListener. (Fortunately, Netscape Navigator is dead.)

In practice, people usually use non-capturing handlers and let events bubble, because “lower” elements can stop the bubbling before it reaches “higher” elements. This means a click on the A can do one thing, while a click on the surrounding P or BODY does another. A keypress in a text field can do one thing while a keypress not in any text field does another.

Event objects have a property event.target, which is where the event actually occurred. In the example here, that's always the SPAN. The property event.currentTarget says where the event is currently being handled, and it would range between the BODY, P, A, and SPAN.

Event delegation is not a browser feature, but a popular technique built into libraries like jQuery. Many blogs get confused talking about it or equate it with bubbling, but I hope the following description is clear.

Suppose you want to respond to a click on any A tag on the page, even if the set of A tags changes over time. In particular, you don’t want to visit every A tag and add an event listener. So, taking advantage of bubbling, you bind a single event handler to the BODY, and from this handler you look at event.target to see if an A was clicked, and if so, which one. Be careful, though, because event.target may be the SPAN! You need to not just check if the event's target is an A tag, but also walk up the DOM tree in a simple simulation of bubbling.

This is event delegation. The BODY element is the delegate that handles events on behalf of the A tags. Conceptually, we’d like to think of the event handler as being on the A tags, so we create that illusion as much as we can. To that end, the final step in event delegation (at least in jQuery and Meteor) is to set event.currentTarget to the A tag. Further code that handles the event then sees an A tag as currentTarget and a SPAN tag as target. The BODY element is not really important, so it is nowhere to be found.

As you can see, event delegation may involve up to three different elements at any given time, here the SPAN, the A, and the BODY. They are the event target; the simulated currentTarget where we pretend the handler was called; and the actual currentTarget where the browser called into the delegation code. In many situations, the first two will be the same (e.g. if there were no SPAN). The third is always different (it’s some enclosing element) and relatively unimportant.

In jQuery, event delegation is performed using “on” with a selector as the second argument:$("body").on("click", "a", handler).

In summary, the steps of event delegation are:

  • Add an event listener on some element enclosing all the elements where you want to simulate event handling (BODY)
  • In the handler, simulate bubbling and look for matching elements (all A tags)
  • When you find a matching element, set event.currentTarget to it and call subsequent handling code

As you can imagine, there are some thorny details that will differ between implementations (like the ordering of delegated and non-delegated event handlers), but in practice these details aren’t too important as long as they reflect simple and consistent choices.

Non-bubbling events

So far, I’ve assumed that all events bubble, and the implementation of delegation that I’ve described relies crucially on bubbling to work at all. However, not all events bubble! Next to each event in the W3C specs, it says “Bubbles: Yes/No,” and there is a long list of events. How can we delegate non-bubbling events? Event capturing seems like an obvious tool, but it isn’t supported in IE 6/7/8 — browsers which are a key focus of libraries like jQuery.

Well, as it turns out, jQuery doesn’t delegate non-bubbling events; it cheats. Most events that people care about and might want to delegate are bubbling events, and if you squint, you can make the rest fit.

Traditional non-bubbling events fall into the following categories:

  • change/submit/reset — These form events don’t bubble in IE 6/7/8, but they do in all other browsers, so jQuery makes them bubble in all browsers (using hacks tailored to these events).
  • focus/blur — These events don’t bubble, ever. Since basically all other UI events do, why don’t these? Apparently, it’s because the browser window can also be sent focus/blur events. As a consequence of not using different names for these events, and the fact that the window is in the bubbling chain, focus/blur events were made non-bubbling. OK, now comes the cheating/squinting part. With this backstory in mind, jQuery defines bubbling versions of focus/blur under new names: focusin and focusout (implemented using some hacks). It then treats focus and blur, when you go to bind them, as aliases for focusin and focusout. If you say to bind “blur,” it binds “focusout,” and it even delivers the event as “focusout.” Developers are encouraged to simply use “focusout” rather than “blur” to avoid any confusion about what’s happening.
  • load/error/resize/readystatechange/… — Other events that don’t bubble are fired on the window or other non-DOM objects like XHRs, so delegation isn’t really relevant. Image tags have a “load” event in some browsers, but it’s not reliable enough to be useful.

So until recently, any event you might want to delegate — in particular UI events like mouse, keyboard, focus, etc. — was bubbling or had a bubbling version in jQuery.

Recently, however, HTML5 technologies have been introducing a lot of new events, including non-bubbling ones. For example, the “play” event on a VIDEO tag does not bubble. As far as I know, jQuery provides no way to delegate such non-bubbling events.

Fortunately, if event capturing is available, it’s not too hard to perform event delegation for these non-bubbling events yourself (and you can skip the step where you have to simulate bubbling). Event capturing is not available in IE 6/7/8, but neither are “play” events! So it’s reasonable for an event system to take jQuery’s approach for cross-browser support, but use event capturing for non-bubbling events in modern browsers. This is Meteor UI’s approach.

Originally posted on Quora.

--

--

Meteor is an open-source platform for building top-quality web apps in a fraction of the time, whether you're an expert developer or just getting started.