The Javascript Event Loop explained!

In JavaScript, almost all I/O is non-blocking. This includes HTTP requests, database operations and disk reads and writes; the single thread of execution asks the runtime to perform an operation, providing a callback function and then moves on to do something else. When the operation has been completed, a message is enqueued along with the provided callback function. At some point in the future, the message is dequeued and the callback fired.

While this interaction model may be familiar for developers already accustomed to working with user interfaces – where events like “mousedown,” and “click” could be triggered at any time – it’s dissimilar to the synchronous, request-response model typically found in server-side applications.

Technical Implementation

Javascript Engine

There are three important features of a JavaScript engine that deserve mention. These are the Stack, the Heap, and the Queue. Now, different browsers have different JavaScript engines (e.g. Chrome has V8, Firefox has OdinMonkey, and IE has something written in BASIC called Chakra (just kidding!)) and each browser will implement these features differently, but this explanation should work for all of them.

Heap: The simplest part of this is the Heap. This is a bunch of memory where your objects live (e.g. variables and functions and all those things you instantiate). In the presentation I refer to this as Chaotic, only because the order doesn’t really matter and there’s no guarantee with how they will live. In this heap, different browsers will perform different optimizations, e.g., if an object is duplicated many times, it may only exist in memory once, until a change needs to happen, at which point the object is copied.

Stack: This is where the currently running functions get added. If function A() runs function B(), well you’re two levels deep in the stack. Each time one of these functions is added to the stack, it is called a frame. These frames contain pointers to the functions in the heap, as well as the objects available to the function depending on its current scope, and of course the arguments to the function itself. Different JavaScript engines likely have different maximum stack sizes, and unless you have a runaway recursive function, you’ve probably never hit this limit. Once a function call is complete, it gets removed from the stack. Once the stack is empty, we’re ready for the next item in the Queue.

Queue: A JavaScript runtime contains a message queue, which is a list of messages to be processed. To each message is associated a function. When the stack is empty, a message is taken out of the queue and processed. The processing consists of calling the associated function (and thus creating an initial stack frame). The message processing ends when the stack becomes empty again.

It’s worth mentioning Garbage Collection here as well. In JavaScript it’s easy to create tons of objects all willy nilly like. These get added to the Heap. But, once there is no scope remaining that needs those objects, it’s safe to throw them away. JavaScript can keep an eye on the current stack and the items in the Queue, and see what objects in the Heap are being pointed to. If an object no longer has pointers to it, it is safe to assume that object can be thrown away. If you aren’t careful with how you manage your code, it’s easy to not have those pointers disappear, and we call this wasted memory a Memory Leak.

Event Loop

The event loop got its name because of how it’s usually implemented, which usually resembles:

queue.waitForMessage waits synchronously for a message to arrive if there is none currently.

The decoupling of the caller from the response allows for the JavaScript runtime to do other things while waiting for your asynchronous operation to complete and their callbacks to fire. But where in memory do these callbacks live – and in what order are they executed? What causes them to be called?

JavaScript runtimes contain a message queue which stores a list of messages to be processed and their associated callback functions. These messages are queued in response to external events (such as a mouse being clicked or receiving the response to an HTTP request) given a callback function has been provided. If, for example a user were to click a button and no callback function was provided – no message would have been enqueued.

In a loop, the queue is polled for the next message (each poll referred to as a “tick”) and when a message is encountered, the callback for that message is executed.

Javascript Event Loop

The calling of this callback function serves as the initial frame in the call stack, and due to JavaScript being single-threaded, further message polling and processing is halted pending the return of all calls on the stack. Subsequent (synchronous) function calls add new call frames to the stack

Implementation Example

Javascript Loop Example

In this example, the very first thing that happens is that function a() and b() are “hoisted” to the top of the script, and are added to the heap. We then run the first message log “Adding code to the queue” in the current stack. After that we run a setTimeout, and the anonymous function in there is added to the Queue. Then we do another log, and run the a() function with an argument of 42. We are now one level deep in the stack, and that frame knows about the a() function, the b() function, and its argument of 42. Within a() we run b(), and we are now two levels deep in our stack. We print more messages, leave b(), leave a(), and print a final message. At that point, our stack is empty and we’ve run all of our code, and are now ready for the next item in the queue.

Once we’re in the next queue item, we run the anonymous function (which exists in the Heap somewhere), and display our message.

At first glance, one might assume the message “Running next code from queue” could have been run earlier, perhaps after the first message. If this were a MultiThreaded application, that message could have been run at any point in time, randomly placed between any of the outputted messages. But, since this is JavaScript, it is guaranteed to run after the current stack has completed.

Conclusion

There you have it, the JavaScript Event Loop. It is great for I/O bound applications, and horrible for CPU bound applications. Many people think the engine is MultiThreaded, or at least that it can do things in parallel. Turns out it can do I/O in parallel, but not CPU computations (unless using a separate process with Node.js or a Web Worker in the browser).