Knee-deep in Javascript

Javascript is a quirky old thing. It divides opinions like no other language, yet it is an integral part of the internet and therefore our lives. Having initially hated it and put off learning it for as long as humanly possible, I am now a doting fan. It's scars and wrinkles have turned to powerful beauty.

This article is going to look at one high level area:

  1. What exactly is happening when you run a piece of code in Javascript?

Let's start with a piece of code:

function logger(message) {
  console.log(message);
}

logger('Hello');

We all know that this will print 'Hello' to the console but what happens behind the scenes? For that, we need to understand the Javascript runtime.

Let's start with the execution context. The Global Execution Context is the name for the environment in which javascript parses, stores and executes code. It has two parts:

  • The Thread of Execution - this is where javascript parses the code line by line, in order.
  • Global Variable Environment (or Global Memory) - this is where javascript stores all the variables and functions that it encounters

As Javascripts thread of execution parses through our code it does one of two things:

  1. Stores code in the memory - resulting in a label and the stored piece of code (note: when storing the code, Javascript doesn't bother to read it. It just puts it in memory to be accessed later)
  2. Executes code - this is where Javascript performs an action such as invoking a function. For this, a new Local Execution Context is added onto the call stack and the thread of execution enters the local context

This brings to attention two points about Javascript:

Javascript is single threaded and synchronous

Single threaded: Javascript follows one single route through the code rather than taking multiple different routes simultaneously.

Synchronous: Javascript processes code in the order that it is presented. Whilst it is processing the current line of code, all the other code has to wait until it is finished

Let's look at this the context of our code snippet:

The thread of execution will pass through our code line by line. The first line it sees is the function definition for logger. Without reading what is inside, Javascript will add that definition to memory under the variable name logger. Then the thread of execution will move on to the next line of code which is line 5. Here Javascript sees the variable name logger and checks the global memory to see if there is a variable saved with the same name. There is, so Javascript executes that code, passing in the argument "Hello".

When the thread of execution invokes a function, Javascript creates what is called a local execution context.  The Local Execution Context or function level execution context is like a mini Global Execution Context. The thread of execution passes through this local context and can store data in its local memory (the local variable environment). Note: the local context can access the Global Memory but the Global Memory cannot access the Local Memory. This is known as scope. As your programs get more complex, you will likely have many execution contexts nested on top of each other. It's important to remember that that each execution context has access to their own memory and the memory of it's ancestors. When it looks for a variable, if it doesn't find it in it's own execution context Javascript will search the next execution context up and so on until it if finds the variable or throws an error.

The Callstack keeps track of which context the thread of execution is in. New local execution contexts (known as calls or frames) are pushed onto the stack and when completed they are popped off the stack and the thread of execution returns to the global context. The Callstack is LIFO: last in, first out like a stack of plates - the last one on is always the first on off.

Given that Javascript is synchronous, multiple blocks of code cannot be executed at the same time. To manage this, Javascript has two queues where asynchronous callbacks wait until the thread of execution is ready for them:

  • The Callback queue (or task queue)
  • The Microtask queue

The Callback Queue and the Microtask Queue are both FIFO: first in, first out. The Microtask queue has higher priority than the Callback queue so its callbacks are executed first. Callbacks from the Callback queue will only be pushed to the stack if the Microtask queue is empty which means too many callbacks in the Microtask queue can starve the callback queue and prevent its tasks from getting onto the callstack.

The Event Loop is responsible for deciding which callbacks are push onto the stack. It uses two criteria:

  1. Has all the code in the global context been executed?
  2. Is the callstack empty?

If these criteria are met, it will push callbacks onto the stack.

So which task makes it into which queue? There are nuances based on each Javascript runtime so let's take a broad brush and assume that we are in the browser.

There are thee main ways of adding to the Callback queue:

  1. Loading a script from your HTML
  2. An event fires
  3. Using setTimout() or setInterval()

For the Microtask queue, you have two options:

  1. Resolve or reject a promise
  2. Use window.queueMicrotask() (although generally you wouldn't use this unless creating a library/framework)

That was a whirlwind tour of the Javascript runtime. With this foundational mental model we can move on to learn about hoisting and async Javascript. Part 2 coming up.