The JavaScript Event Loop

Digging into the guts of JavaScript's event loop.

Scroll down...

Content

Resources

Comments

You've had a chance to play around in the DOM and it lets you do some pretty cool things. But you'll really see the power of jQuery when you learn about browser events. If DOM manipulation is the bread and butter, events are the cake.

Your browser is one big infinite loop. It's waiting for you to do things all the time and, when you do, it fires off "Events" to describe what you just did. Some events are generated by the user, like clicking, hovering, moving the mouse, typing keys, and so on. Others are generated by the browser or the life cycle of various requests. You've already worked with one of these -- the $(document).ready() function listens for the ready event that your browser sends when it has finished loading the entire DOM.

Your JavaScript code can register "listeners" for those events, which are functions that get invoked when the event is detected.

In this lesson, we'll show you how JavaScript's event loop works and how this interacts with the browser. It's a bit more technical than most of what we cover but it's provided so you can get a good mental model for what's actually happening when you register a new listener or, as you'll see later on, when you start making asynchronous HTTP requests with AJAX.

Our goal isn't for you to remember all the technical details -- they are provided as supporting details so you can build your mental model. In the next lesson, we'll look at the dead-simple ways of actually implementing this stuff with jQuery.

Why We Need Asynchronous Behavior in the Browser

To understand how events are handled, it's worth taking a step back and looking at how JavaScript's own event loop works. This is the feature that gives JavaScript the ability to act asynchronously and is a defining characteristic that makes it an attractive language for other applications like server-side programming (via Node.js) as well.

This asynchronous behavior is necessary because the kinds of events that JavaScript works with are often either time consuming (HTTP requests) or highly intermittent (mouse clicks). Your browser is literally bombarded by different events all the time. If each of these events blocked other functions from executing, you'd be in a world of trouble.

Let's say you click a button and it makes a network request that takes a long time to run. If the process was truly single-threaded and synchronous, the button wouldn't even be able to repaint itself in the raised position as "unclicked" until the request returned.

We'll look a bit more at how long certain operations take in a minute. First, though, let's lift the hood and see what's actually going on with JavaScript.

The Guts: The Call Stack and Message Queue

We'll start with the basics. JavaScript is single-threaded yet it mimics the behavior of a multithreaded process by efficiently using its time when it comes to asynchronous processes. The JavaScript runtime environment does this by prioritizing tasks between its Call Stack and Message Queue.

Stack Heap Queue diagram

The Call Stack contains the list of messages (method calls) that need to be executed RIGHT NOW. Each of these messages is called a Stack Frame when it sits on the call stack. A new frame is created every time the runtime encounters a method call or any other scripted behavior like an event firing. If a method that is being executed calls another method, this new message is added to the top of the stack, now taking full priority. When a message returns (with or without a return value), its frame is removed from the stack.

The runtime is constantly looping through the call stack and executing the frames from top to bottom. Because JavaScript is single-threaded, executing a frame takes its full attention. But this happens extremely quickly, so it will often work through the stack and get to the bottom.

A few real-world points of interest about the stack:

  1. You should also be able to visualize now what's happening when a program is executing -- and how having methods calling methods calling methods will just keep the stack growing until the deepest level returns, then the stack gradually shrinks back down as each successive method returns until it disappears when the last message returns.
  2. You can now see how creating an infinite loop where a function keeps calling itself can cause the stack to infinitely increase... and create a "stack overflow" condition.

Other popular languages really only use a call stack -- where the interpreter just chugs through everything in a linear order. It's the addition of the queue that allows JavaScript to start doing interesting things.

When the JavaScript runtime gets to the bottom of the stack, it goes over to ("polls") the message queue and dequeues the first message that was added. Whereas the Stack is always executing the latest item placed on top (LIFO), the Queue is a first-in-first-out (FIFO) system where the message that's been waiting the longest gets executed first. Once a message is pulled from the queue, it is added to the stack for immediate execution.

When you have a queue, you can offload messages that are not immediately necessary to the back of the queue. In JavaScript, all new messages are placed at the end of the queue. That's what happens when the browser detects an event, for instance, a mouse click. When this click is detected, the interpreter checks to see if anyone is listening for that particular event. If they are, it grabs the callback function that was provided by the listener and puts it at the end of the queue for running.

Asynchronous JavaScript

If you're paying attention, this isn't actually asynchronous -- it's still synchronous. And that works perfectly fine most of the time. Most of the time your JavaScript runtime loop is just polling an empty queue, waiting hopefully for something to happen. When it does get a message to put on the stack and run, that message is almost always completed quickly enough that it doesn't affect anything else.

The problems occur whenever you do I/O operations like writing to cache, writing to memory, writing to disk, or, worst of all, making a network request. The relative amount of time these things take is below:

  1. L1-cache: 3 cycles
  2. L2-cache: 14 cycles
  3. RAM: 250 cycles
  4. Disk: 41,000,000 cycles
  5. Network: 240,000,000 cycles

That's right -- making a network request is a million times slower than accessing RAM. But all of these take finite amounts of time. If you're waiting for a network request to complete and only have a call stack to work with, your application basically just goes to sleep until it returns.

In JavaScript, all of these I/O operations can take advantage of message queuing to become asynchronous. That's because they can be run as a "non-blocking call", which means they actually exit from the call stack and place their callbacks in a separate message queue which is maintained by a third-party process like your browser (which has its own thread(s)).

For example, if you're making an "XHR" (AJAX) network request in the browser, JavaScript will offload its callback to a separate queue that's taken care of by the browser. When the request finishes, the browser detects this and places the callback back in JavaScript's queue to run normally. In the meantime, life goes on as normal for your JavaScript's runtime and it can continue to process the stack and queue as normal.

Other types of I/O might use different queues, which are usually provided by your browser's "Web API".

SetTimeout(...,0)

There's a trick you sometimes see JavaScript developers use to run a method asynchronously. To do so, pass the method as a callback to the setTimeout function, which delays execution by a specified time interval. The callback function gets passed over to your browser's timing API and is sent back after the specified delay.

If you delay 0ms, the browser immediately adds your callback function to the end of the queue. Think of using setTimeout as an ejection seat from the current call stack straight to the back of the queue. Or not "straight" to the back of the queue if you provide a non-zero delay interval.

While the technical details may be more than you need to know, it's a useful trick to be able to make a function non-blocking:

console.log("I'm executed first!");
// note the delay of 0ms.  You can experiment
// with passing this different values.
// 1000ms = 1s
setTimeout(function(){
  console.log("I'm at the end of the queue now!");
}, 0);
console.log("I get executed before");
console.log("JavaScript goes checking for");
console.log("any new messages on the queue");
console.log("so myNonBlockingFunction will not");
console.log("get run until right...");
console.log("about...");
console.log("Now!");

See a visualization of this example here.

See the Resources tab for some additional reading and video explanations of this if you're curious.

Callback Scope

We'll cover scope in greater detail later on, but our stack and queue model is particularly relevant now when you consider event listener callbacks. We just described how a typical message enters the JavaScript message queue and then its callback is executed as a new message separately from the original message. What variables are local to the callback? How much information does the callback "know" about the original environment now that it's on its own?

Those are the questions of Scope. In practice, the call stack and environment variables needed by the callback are all packaged up nicely into a Closure and preserved for when they're required. Again, we'll get into this a bit more later on.

For a good explanation of all this stuff, see this blog post from Carbon Five.

Registering Listeners

If you think of the JavaScript event loop in the context of the browser, things get more interesting.

The browser fires off events like mouse clicks all the time. Most of them fall on deaf ears and don't do anything special. As a developer, you can use JavaScript to register an Event Listener, which sends the callback for that listener into a special queue that your browser maintains. When the specified event is fired, your browser identifies the eligible callbacks for you and sends their callbacks over to JavaScript's message queue for running.

The old fashioned way to add event listeners was to put them directly into the HTML markup by adding properties like onclick which could execute code immediately or call other JavaScript functions. This is NOT considered good practice because it tightly couples your JS and markup but you'll occasionally see it used with quick-and-dirty hacks:

<button onclick="alert('Hello')">Say hello</button>

A more conventional example of this in pure JavaScript is using the addEventListener function. In the example below, we've added an anonymous "click" listener to the first paragraph element on the page:

// Note that we need to retrieve the first
// element from the collection we'll be given
document
  .getElementsByTagName("p")[0]
  .addEventListener("click", function(){
    console.log("Clicked the first <p>!");
  });

The anonymous function we've provided to addEventListener gets stored by your browser and is sent back to JavaScript to be run whenever a "click" event is detected on that specific element.

Using listeners is really that simple.

Code Review

The important bits of code from this lesson

// Eject to the back of the queue
setTimeout(function(){...}, 0);

// Add a click listener the hacky way
<button onclick="alert('Hello')">Say hello</button>

// Add a click listener the JavaScript way
var myEl = document.getElementsByTagName("p")[0];
myEl.addEventListener("click", function(){
    console.log("Clicked the first <p>!");
  });

Wrapping Up

This lesson got a bit technical but hopefully, you've got a good idea now of how JavaScript processes events and how this allows it to perform asynchronous behaviors. You should also see that listeners are just callback functions registered with your browser and added to JavaScript's message queue. In the next lesson, you'll see the wonderful variety of listeners you can set up and how easy they are to work with via jQuery.



Sign up to track your progress for free

There are ( ) additional resources for this lesson. Check them out!

Sorry, comments aren't active just yet!