JavaScript concurrency model and the event loop

 • 

If you have spent any amount of time using JavaScript you will probably have come across the fact that commonly JavaScript implementations are implemented in a single threaded environment. This means that there is no facility for programmers to make use of concurrent programming the way we're used to from languages like Java or C.
The JavaScript interpreter goes through the code line by line and executes every line in the same thread or 'context'.

first();
second();

In the above example second() will not be pushed on the call stack until first() has finished and returned. There is no way to execute first() in a different thread and run these two functions concurrently (nope, I've never heard of Web Workers).

Problem?

One problem with this approach is that
long lasting and CPU intensive operations will block the entire process, and in the case of JavaScript being executed in the browser, make the UI unresponsive.
The takeaway from this is, that you should avoid writing CPU intensive code in JavaScript. It just isn't made for that kind of code. If you need to perform CPU intensive computations, those should be done in another process. Note, that while no threading mechanisms are exposed to the programmer, the VM itself does work with threads in the backend.

Note that while all your own code is executed by the JavaScript VM in one thread all I/O operations are performed asynchronously to avoid blocking the single thread available. Asynchronous I/O lends itself particularly well to an event driven programming paradigm, which just so happens to be used in JavaScript.

Registering events and the event table

Since JavaScript is an inherently event-driven language there are many different ways for the programmer to register events.

  1. EventTarget.addEventListener(type, listener)
  2. <div onclick="eventHandler" ...
  3. element.onclick = () => {...
  4. setTimeout(() => {..., 1000

The listener is simply a callback function to be invoked once the event fires.
When a listener is registered on an event target, the event in question is added to the event table along with the listener callback.

Once the event fires, the table enqueues a message including the callback function of the event that fired to the event queue. It does the same for all other listeners that were registered to the fired event.

The event table can be thought of as a staging area for event listeners whose events haven't fired yet, waiting to be moved over to the event queue.

The event queue and the event loop

The event loop checks whether there is a message in the event queue. If so, it dequeues that message and invokes it if the call stack is empty.
This part is crucial: The event loop only invokes a function off of event queue if the call stack is empty.
This is in line with the non-preemptive nature of JavaScript.

The event loop synchronously waits for messages on the event queue and processes them in order of arrival.

while (queue.waitForMessage()) {
  queue.processNextMessage(); 
}

Everything in action

Let's take a look at an actual example and step through the sequence of events (no pun intended) with what we now know about the event loop and queue in JavaScript.

function firstFunc(){
   console.log('First');
}
function secondFunc(){
  setTimeout(firstFunc, 1000)
}

secondFunc()
console.log('Yello')	
  • FirstsecondFunc is pushed on the call stack.
  • Then setTimeout is pusehd on the callstack above secondFunc.
  1. setTimeout registers firstFunc as listener adding it to the event table along with the event that will fire after 1000 milliseconds from now. setTimeout returns as is popped of the call stack, leaving only secondFunc on the stack.
  • The JavaScript engine then continues executing the next lines of code, in this case console.log('Yello').
  • and moving the event along with the function over to the eventQueue
  • Once 1000 milliseconds have passed, the previously registered event fires, at which point the event table moves the registered callback to the event queue.
  • The event loop, which synchronously waits for new messages to arrive in the queue, dequeues the first message and pushes the function on the stack if it is empty.