JavaScript and Events - The Fundamentals

Reacting to browser events in JavaScript is one of the fundamental ways to start building more reactive websites and rich web applications. Get started with this primer.

Understanding how the language and the browser interact is important if you want to get the most out of JavaScript and save yourself a bunch of future headaches. We'll cover all the basics about browser events and point you toward other resources so you can dive deeper.

JavaScript vs. the browser

JavaScript is a compact programming language that is single-threaded. That means all the action happens in the same pipeline. In operating systems and some other programming languages, you can do multiple things at once, but JavaScript can only do one thing at a time.

Because of that, JavaScript code can be described as blocking. Only one thing can happen, and the next thing JavaScript wants to do can't start until the first thing ends.

JavaScript on our websites runs in the browser's JavaScript engine and will do things like arithmetic and logical flow (if blocks, for loops, while loops), and it has some built-in data types like strings, numbers, objects, and arrays.

On the other hand, you have browser APIs. The browser API is an extensive and growing set of capabilities the browser has built-in. Some examples:

  • DOM - the Document Object Model and all the methods that go along with it to allow manipulation of HTML.
  • CSSOM - the CSS Object Model which allows manipulation of CSS
  • Observers (like intersection observer and resize observer)
  • Fetch - an interface for fetching resources 
  • History - allows the navigation and manipulation of the contents of a browser's history

All of these APIs are part of the browser.

The event loop

The event loop is how JavaScript and the browser interact.

Chart showing the event loop, displaying the javascript call stack, web apis, and callback queue and how they interact.

The Javascript event loop

The JavaScript call stack

Remember that JavaScript is blocking and single-threaded. The call stack is how functions are organized, and a second task can't begin until the first task ends. This order remains true…unless the first task calls another function.

If a function is called during the execution of a task, it gets placed on top of the call stack. If we call another function within that function, it will also be placed on top of the stack. Since it's a stack, we can only pull from the top of it, so we can't pull things off the bottom of the stack to execute until those top pieces are resolved. This way of dealing with tasks is an approach known as last-in-first-out or LIFO.

When we describe code as "blocking," this is the source of that problem. Functions running in the call stack prevent other tasks from being completed.

The callback queue

To avoid blocking other functions, JavaScript can offload work to Web APIs. If you want to make an AJAX call or set a timeout, you can send that information to the Web API to do all that work. When the API has done its work, if it has a callback function to run, it goes into the Callback Queue. Once JavaScript has run everything in its call stack, it pulls the next task from the callback queue, runs that to completion, then grabs the next thing in the queue.

If you want to dive into this deeper, you can watch this video about the Event Loop. It demos a website called Loupe, a simulated event loop in the browser.

The event loop leads us to another way JavaScript offloads work to a browser API: events.

Browser events

To reiterate, the Event Interface is a Web API. Events are a browser thing, not a javascript thing. The browser has events and kicks them off, and JavaScript is given the ability to respond to those events. Events allow the browser to kick off programmatic responses to interactions, changes, and other significant happenings.

Events can be triggered by user interaction, browser behavior, and your code. User interaction events are triggered by things like mouse clicks, buttons pressed on the keyboard, and elements gaining or losing focus. Browser behavior events happen for things like loading and unloading resources and changes in device orientation. Finally, you can artificially set off events from your code with the .dispatchEvent() method.

View the full reference for available events.

Handling events in JavaScript

So events happen on the browser side. Where does JavaScript come in?

To react to events, you’ll use EventTarget.addEventListener(eventtype, callback).

An EventTarget is, for the most part, any element on your page. The class is broader than that, but for our purposes, this is enough. We can say "select my button" or "select my paragraph" and add an event listener to it.

That short line of code is saying: "Hey browser, when 'EventTarget' has an 'eventtype' event, can you run the function 'callback'?"

Let's walk through what happens when someone visits your page:

  • Server/Cache delivers a raw .html file.
  • Browser parses that HTML into the DOM.
    • This is where inline JS runs.
  • Browser performs prefetches.
  • Browser does layout/paint operations.
  • Browser fetches remaining resources.
    • This is where external JS runs. If you have put your code in an external file, this is when your JavaScript code is loaded and run.
  • Running js calls addEventListener. Let's say you want to react to a button click.
  • Browser stores callback function.
    • An indeterminate amount of time passes. Right now, everything is sitting over in the Web API.
  • The specific event happens on the specified target (the button click).
  • The browser drops the callback function into the callback queue.

What if there are multiple events?

When a click happens on an element, you have mousedown, mouseup, and click events all happening in quick succession, all on the same thing. How do we know things happen in the correct order?

The browser fires off events in a predictable pattern that happens in up to three phases: capture phase, at target, and bubble phase. Think of event phases like a ball bouncing. The ball falls, makes contact with the ground, and then rebounds.

Before going into more detail, it will help to distinguish how different types of people perceive a web page versus how the browser perceives it. Designers might see components coupled in layouts. Content strategists might see headings, articles, and footnotes. Developers might see sets of nested elements containing text and the abstracted code behind them.

The browser doesn't care about any of that. It cares not about content or presentation. It only cares about the DOM. The browser stores its knowledge of the page's elements like a flow chart in a tree structure, relating each node in the tree to every other node. Events always start at the top of the tree and work their way down toward the deepest event target.

An HTML DOM tree with a red line showing how an event traverses the tree until reaching its target.

The event will traverse the tree until it reaches the anchor element, its target. (Image courtesy of Birger Eriksson (WikiMedia))

Imagine a click event on the anchor tag element near the bottom of the diagram. The event starts at the top and digs deeper.

An HTML DOM tree with a faded red line showing how an event traverses the tree until reaching its target, with a giant red dot over the target, the anchor element

The event has reached it target. (Image courtesy of Birger Eriksson (WikiMedia))

 Once the deepest possible element has been reached, the event fires from the "target" element. In this case, "target" means what the browser was aiming for, not necessarily the user/developer. If a user is aiming for one thing and misses, the target is whatever they actually clicked on.

An HTML DOM tree with a red line showing how an event traverses, or bubbles, back up the tree.

After hitting the target, some events bubble back up the DOM tree. (Image courtesy of Birger Eriksson (WikiMedia))

For some events, you have a bubbling phase. It retraces its path back up through the DOM toward the root. Not all events bubble. There's not a handy way that we've found to remember which events bubble and which ones do not. You'll need to check the MDN documentation.

It's important to note the event itself "moves" rather than having a cascade of events. Instead of having a series of events fire, one per DOM node as the browser works down towards the target, the browser has a single event that is "retargeted" on each DOM node in succession. Because of this, it is important to react quickly in any event listeners if you need to alter or stop the event before the browser retargets the event to the next DOM node.

Why does all of this matter?

When you add an event listener, you can tell the browser which phase to invoke your callback function on. Inside your callback function, you can alter how the browser handles the event by:

  • Preventing default behavior
  • Preventing the event from continuing its path along the DOM
  • Preventing the event from invoking any additional callbacks on the current target, if you have things in the correct order

Adding event listeners

Let's look at some code. If we want an event listener, we first need to get a reference to the element that is our event target. In this case, it will be the first button on the page.

const button = document.querySelector(‘button’);

We have three ways to set a callback on a click event. The first way has a named function declared elsewhere.

function myCallback(event) {
  console.log(event);
}

button.addEventListener(‘click’, myCallback);

The second way passes an anonymous function in the older syntax.

button.addEventListener(‘click’, function(evt) { console.log(evt) });

The final way is with an arrow function.

button.addEventListener(‘click’, (e) => { console.log(e) });

While these examples all do the same thing, they will be treated as three different event listeners. If we run this code, clicking the button would result in 3 identical console log statements.

The default behavior for events:

  • Event listeners will always fire on the target phase.
  • Event listeners will fire on the bubble phase by default on events that bubble. The event will go down the DOM until it hits the target, but even listeners on the target's parent will not fire until the bubble phase when the event retraces its path.
  • Multiple event listeners on the same target will be triggered in the order they were attached.

Modifying default behavior

Event listeners can be set to run on capture. As the event passes an element, it triggers the callback function before the event gets to the target.

.addEventListener(‘click’, callback, true);

.addEventListener(‘click’, callback, {useCapture: true});

Event listeners can also be set to remove themselves after firing.

.addEventListener(‘click’, callback, {once: true});

Responding to events

All events have these properties/methods.

Event = {
  bubbles: boolean,
  cancelable: boolean,
  currentTarget: EventTarget,
  preventDefault: function,
  stopPropagation: function,
  stopImmediatePropagation: function
  target: EventTarget,
  type: string,
}

The currentTarget property points to the current event target as the event moves through the DOM and will change for the event as it is retargeted. The target property is the event's intended target. The type will be the name of the event, like "mousedown" or "click."

Some events that would normally trigger a default browser action can have that action prevented by calling the preventDefault() method. For example, if you have a link with a click event listener, you can prevent the browser from navigating away from the current page. While this is not a great idea for accessibility purposes, it is a common enough occurrence to serve as an example.

function myCallback(event) {
  event.preventDefault();
}

Events that might trigger additional callbacks on elements further along the path can be halted with the stopPropogation() method. Calling it will stop the event from being retargeted further. Stopping propagation is a valuable tool to manage events that might otherwise conflict or overlap. For example, a click event listener on a parent element and a button inside, and you want to listen to one set of clicks but not another.

function myCallback(event) {
  event.stopPropagation();
}

With stopImmediatePropagation(), you can also stop any additional callbacks on the current element that might come after the current callback. Calling this method is not common and requires careful consideration of the order in which listeners are added.

function myCallback(event) {
  event.stopImmediatePropagation();
}

Certain event types have additional relevant properties. Click events get the coordinates and modifier keys. Keyboard events get keycodes. Focus events get the element that the focus moved from/to.

Removing event listeners

You can remove event listeners if you only want to listen for a limited amount of time or a specified number of times. The trick is to have a reference to the exact same function in the add and remove methods. You must have named callback functions if you want to do this. Anonymous functions will not work, even if they are character-for-character matches.

The following code works:

function myCallback(event) {
  console.log(event);
}

button.addEventListener(‘click’, myCallback);
button.removeEventListener(‘click’, myCallback);

These examples do not work:

button.addEventListener(‘click’, function(evt) { console.log(evt) });
button.removeEventListener(‘click’, function(evt) { console.log(evt) });

button.addEventListener(‘click’, (e) => { console.log(e) });
button.removeEventListener(‘click’, (e) => { console.log(e) });

Discovering and debugging event listeners

All major browsers have a way to discover event listeners in the devtools. Because the Web API depends on the browser's implementation, it can be different for each browser. The following image is what it looks like in Chrome.

How Chrome organizes declared event listeners.

Chrome organizes by type of event. We can see that there are two click listeners and one keydown listener currently visible.

With Safari, you need to look at the Node section.

Devtools in safari showing event listeners

Firefox puts a little event badge in the inspector, which will show you all of the event listeners on that one node.

devtools in firefox showing event badges on elements, and the pop up event listeners

Conclusion

Reacting to browser events in JavaScript is one of the fundamental ways to start building more reactive websites and rich web applications. This article has been a primer on the fundamentals that should save you some headaches down the road. Here are some additional resources:

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!