Sai Teja
LightMap

LightMap

How Does JavaScript Even Work? Things Which 90% of JavaScript Developers Don't Know! (Part 1)

How Does JavaScript Even Work? Things Which 90% of JavaScript Developers Don't Know! (Part 1)

ECMA Specifications, JS Engines, Single Thread, Asynchronous, Web APIs, JS Call Stack, Task Queue and more

Sai Teja
·Dec 28, 2021·

15 min read

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Table of contents

Note: This is Part 1 of the series title "How Does JavaScript Even Work?". I would highly recommend you to read Part 2 as well after reading this part.

How Does JavaScript Even Work? (Part 1)
How Does JavaScript Even Work? (Part 2)

JavaScript was initially developed as a frontend scripting language to fulfil the lacking dynamic behaviour of the web by Brendan Eich of Netscape for their Netscape Navigator browser.

From there, Javascript has today grown into such a huge ecosystem that you can develop applications on the web, mobile and desktop with just javascript which is impossible with any other language.

Now, if you have ever worked with JavaScript, you would have heard that Javascript is single-threaded, non-blocking and it is asynchronous as well.

Isn't that counter-intuitive? How can something be single-threaded and asynchronous as well?

Well, there are a lot of such things in JS and to understand it all, you need a deep dive into JS and its working.

So, let us try and understand the working on javascript which 90% of JS developers don't know!!

Firstly, let us see what is ECMA and what do these specifications tell us about JavaScript.

ECMA Script

ECMA basically stands for European Computer Manufacturer's Association and it defines a trademark standard for scripting languages like JavaScript and the JScript (Developed by Microsoft).

You would have heard ES6, ES7 while working with JS, the ES here stands for ECMAScript.

ECMA puts a lot of specifications in place related to notations, operations, syntax, functions, classes etc.

You can refer to all these specifications here: ECMAScript

image.png

But ECMA doesn't have any documentation on the event loop, non-blocking, asynchronous JS.

Hmm, so where do we go now? We go to the JS engines!!

Note: If you want to read more about the advent of JS and its whole history, here is a fantastic blog: Why are we creating a JavaScript-only World Wide Web?

JavaScript Engines

A JavaScript engine is basically a software that executes JavaScript code. There are various Javascript engines available. The most famous engines among the modern Just in Time Compilation engines are:

  1. V8 ( Developed by Google | Used in chrome, Node.js runtime, Deno runtime)
  2. Spider Monkey (Developed by Mozilla | Used in Firefox)
  3. WebKit (Developed by Apple | Used in Safari)
  4. Chakra (Developed by Microsoft | Used in Internet Explorer)

Check out the list of all JS engines here: List of ECMAScript engines (Wikipedia)

Now, as V8 is the most used javascript engine, from now whatever we discuss will be its implementation. Though the other mentioned engines are also similar with a few differences.

So, the V8 engine is what makes Javascript single-threaded as it comes with a single call stack (also called as the execution stack) or single thread.

V8 Architecture.png

The V8 engine comes with two main components which are

  1. Call stack: This is where all the execution happens.
  2. Heap: This is where the memory allocation happens.

But even standalone V8 doesn't make JS asynchronous.

So, how does it become asynchronous? Keep Reading.

So, let us try to understand the single-threaded nature of JS first and what does it mean. Then we will see how does it become asynchronous and what problem does it solve.

Javascript and the Single Thread

Let us get this single thread concept right!

Javascript being single-threaded means that there is only one call stack provided by the engine. All the execution happens one block after another in a sequential way. That means that all the upcoming code should wait until the current block of code is being executed.

One Thread == One Call Stack == One Thing at a Time

This is called the synchronous execution of code.

Note: Let me tell you that, Javascript can be multithreaded as well for example we can use multiple threads in Node.js using a module called worker_threads.

So, why do people call JavaScript a single-threaded language? It is because of the implementation of JS engines like V8 in the browsers which makes JS single-threaded because these engines have got only one call stack as already mentioned.

Now as you understood this, let us try a few programs with the main thread to understand the call stack and the problem with it.

Understanding the Main Thread and Call Stack

Let us see a few programs and understand the working and the flow of execution of the main thread and the call stack.

Note: For people who don't know about a stack, it is basically a first in last out (FILO) data structure i.e if an element goes into a stack it comes out last. Refer to this link for more about a stack data structure as you need to understand it to understand the call stack.

Working of the Call Stack

Firstly, let us see a simple program with a couple of functions and let us understand the flow of execution just to get the gist of it.

<button id="foo">Foo</button>
<script>
  function bar() {
    console.log("bar");
   }

  function jazz() {
    console.log("jazz");
    bar();
  }
  function foo() {
    console.log("foo");
    jazz();
  }
  const btn = document.getElementById("foo");
  btn.addEventListener("click", foo, false);
</script>

// Output (On button click)
// foo
// jazz
// bar

So, let us see how does the other work with the call stack.

-> A Global Execution Context (GEC) is created and pushed to call stack.
-> We click the button Foo.
-> An execution context is created for foo() and is pushed to call stack and starts executing line by line.
-> Then function foo() logs "foo" and calls jazz().
-> Then another execution context for jazz() is created and pushed on top of call stack.
-> Function jazz() logs "jazz" and then calls bar().
-> The execution context of bar() is pushed on top of the stack which then logs "bar" and returns.
-> Once bar() returns, it is popped out of the stack.
-> Then jazz() and foo() are also popped out as it reaches the end.
-> Once the program ends, the GEC is also popped off and the call stack is empty.

So, that is how the functions are pushed, executed and popped out of the stack.

Global Execution Context: GEC is the default execution context in which JS code starts its execution. It creates a window object and this keyword which binds to the window object for us.

GEC is popped off the call stack once the execution ends and is created again before any ECMAScript code is executed.. ECMA spec for GEC.

Execution Context: Execution context is like a local environment that is created for executing a function with its LexicalEnvironment, VariableEnvironment and the binding of this keyword for that function. Checkout ECMA spec for execution context.

With that said, now, let me show you how you can blow up this stack with infinite recursion.

Blowing up the Call Stack

Let us try an infinite recursion script and also try to find out how many functions can the call stack in our browser can handle at once.

<button id="foo">Foo</button>
<script>
  function foo(i) {
  console.log(i);
    foo(i + 1);
  }
  const btn = document.getElementById("foo");
  btn.addEventListener("click", () => foo(0), false);
</script>

// Output (Can differ from machine to machine)
// Firefox: 0 -> 34753
// Chrome: 0 -> 11400

In here, once we click the button, the infinite recursion starts running and keeps on pushing the function foo() onto the call stack and this blows up the stack.

On execution, you can see the numbers of each function call in the console and these keep coming until the browser throws a nice little red error message saying Uncaught InternalError: too much recursion (Firefox) or Uncaught RangeError: Maximum call stack size exceeded (Chrome)

Cool, but what if I say that we can make the same infinite recursion work with a little modification in the code, what? yes, let us see the same program with little modification in the event loop section.

Blocking Main Thread

Now, let us see what happens if we run an infinite loop on a button click.

Here is the program

<button id="block">Block Main Thread</button>
<script>
  // The function executes on Block Main Thread Button Click
  function blockMainThread() {
    while (true);
  }
  //Adding event listender to the block main thread button
  const btn = document.getElementById("block");
  btn.addEventListener("click", blockEventLoop);
</script>

On the button click here, the blockMainThread() function's execution context is pushed into the call stack and it keeps executing due to the infinite loop. (Note: GEC is always there)

Due to this, the single thread never gets free and all the UI responsiveness gets blocked as the thread is not available to process anything.

Thus on the button click, the tab becomes completely unresponsive and we can't do anything in that tab, we can't even reload until we get the stop prompt by the browser which forces the thread to stop.

Note: The whole procedure of GEC being created and the call stack mentioned in the first program is there for the above programs as well.

The Problem

So, you would have got the problem by now! Though the code we executed was an extreme, there can be a lot of cases wherein a function can take a lot of time and can block all the other code making the page unresponsive for a long time.

There can also be cases wherein we want something to execute only after some stipulated time.

But doing it with a single thread would be the worst user experience!!

So, how do we solve this???

This is where the web APIs, event loop and multiple queues come in to rescue the javascript call stack (single thread).

Let us understand what are web APIs and what all queues exist here and then we will look into the working of Event Loop in detail.

Web APIs

Web APIs are basically a large number of powerful functions and interfaces exposed to us by the browser.

Web APIs are typically used with JavaScript though they are implemented in faster languages, for example, C++ in chrome.

These web APIs enhance JS and give it the ability to do all the powerful things like

  1. Network requests
  2. DOM manipulation
  3. Local storage
  4. Bluetooth
  5. Screen Capture
  6. Location
  7. setTimeout and Timer
  8. And even console.log is also part of console web API

Here is a complete list of Web APIs (Mozilla Network)

image.png

P.S: I know that it can be heartbreaking for developers who are reading this for the first time but yes, all the fetch, setTimeout, console.log are not a part of JavaScript, instead, they come from the browser.

Now, the speciality of the web API is that they are executed with effectively different threads by the browser and do not disturb the main javascript thread of the tab.

While searching for web APIs, I came across a common question that is

Is console.log also executed asynchronously?

as the console is also a web API.

The answer is basically YES, console.log is also executed asynchronously in major browsers. But there is no call back involved here i.e after the console log is offloaded to the browser, it directly gets logged to the browser console and doesn't come back to the JS engine. So, we can't observe the asynchronous nature.

So, like that, all the web APIs are executed effectively in another thread without blocking the main thread and offloads all the slow tasks from the JS engine.

V8-WebAPI.png

Note: The browser gives the JS engine access to all these web APIs via the window object which is a global object created by the Global Execution Context (GEC). Actually, we should access these web APIs like window.setTimeout etc but JS facilitates us by giving direct access to these methods.

Let us see an example of how does adding an event listener works using the DOM API.

<button id="foo">Foo</button>
<script>
  function foo() {
    console.log("foo");
    jazz();
  }
  const btn = document.getElementById("foo");
  btn.addEventListener("click", foo, false);
</script>

-> A Global Execution Context (GEC) is created.
-> The JS code starts executing line by line.
-> It skips the functions and come to document.getElementById("foo"). This calls DOM web API which returns an object reference to the identified element.
-> Then the execution comes to btn.addEventListener("click", foo, false);. Again we call DOM web APIs addEventListener method which is offloaded to the browser.
-> Browser then binds the foo function to the given HTML element and triggers it on a click event.
-> The execution of global code ends and GEC is popped off.

That is how adding an event listener works! Here there was no callback and no need to come back to the JS engine.

But then how do web API talk back to the main thread which is required in many cases like to pass callbacks of network calls, disk access etc.

Here come various queues and the event loop.

Queues

Now coming to queues as mentioned, there are three major queues in the implementation of the event loop here.

The three queues namely are

  1. Task Queue (or Callback queue)
  2. Render Queue
  3. Micro-Task Queue (or Job Queue)

Firstly let us understand the task queue and get an understanding of the event loop with the task queue. Then we will add other queues and see how does the whole system work.

The Task Queue

The task queue, also known as callback queue or message queue, is basically a first in first out (FIFO) data structure (more on queue data structure here).

So, once the web APIs do the heavy lifting and executes the time taking task in the background, it passes the callbacks of these tasks to the task queue also known as the call back queue.

That is it, the job of the task queue is just to hold these callbacks.

V8-WebAPI-TaskQueue.png

What happens next? The event loop comes into the picture.

The Event Loop

Once all the normal JS code is executed and the call stack gets empty, the event loop is then triggered.

The event loop then checks for any tasks in the task queue and if there are any, it pushes it to the call stack and it gets executed.

So, the following is the complete execution flow of a JS program.

  1. Once the call stack encounters a task involving the web API, it asks the web API for help.
  2. The web API asks the call stack to just mark it as done and offload it to the web API.
  3. Web API i.e the browser executes the task in the background and pushes the call back to the task queue.
  4. Once the normal code execution is completed and the call stack is empty with GEC also popped off, the event loop is triggered.
  5. Event loop checks for the tasks in the task queue and pushes the tasks one by one onto the call stack and executes them.
  6. GEC is always created before any javascript code is executed, so even when the event loop pushes a task, GEC is created.

Huh, it would be amazing if you could visualise this, right? Don't worry, some amazing developers have got you covered.

Following is a link to a tool that can visualise the above flow of execution.

Event Loop Visualisation: latentflip.com/loupe

image.png

One small note that I felt you should know is:

Note: Event loop is a general CS term and refers to a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop is used in windows OS, GLib and many other places ( Wikipedia). Just to tell you that this is not something unique to JS. Though the implementation can be different, the concept is the same.

Event Loop Implementation in Chrome

Let me tell you that the event loop is not a part of the V8 engine. Instead, V8 actually allows external event notification libraries to be integrated with it.

In the Chrome browser, V8 comes with libevent which is a very popular open-source event notification library and brings the event loop functionality to Chrome.

Whereas, in Node, we use libUV with V8 for the event loop functionality. We need a little different implementation of the event loop in Node to facilitate its server-side execution, thus, we use libUV there.

libevent: libevent.org (The event loop used by chrome with V8)
libUV: libuv.org (The event loop used by Node with V8)

V8-WebAPI-TaskQueue-EventLoop.png

Event Loop in Action

Let us see the event loop in action with a few programs.

Making Infinite Recursion Work

Remember we did a program that blew away the call stack? Now let us see the same infinite recursion program with a little variation which will work without any issue.

<button id="foo">Foo</button>
<script>
  function foo(i) {
    console.log(i);
    setTimeout(() => foo(i + 1), 0);
  }
  const btn = document.getElementById("foo");
  btn.addEventListener("click", () => foo(0), false);
</script>

If you carefully observe, we just added a setTimeout with the delay as 0 only. Other than that it is the same infinite recursion program. So, let us understand its working.

-> The GEC is created and pushed to the call stack on load.
-> We click the button, Foo which calls function foo().
-> The execution context for foo() is created and pushed to the call stack.
-> The execution of foo() starts and it logs the value of i which is 0 at first call.
-> It encounters setTimeout() which is a web API. It marks it as done, offloads it to the browser and moves on.
-> The execution comes to the end of foo().
-> The execution context of foo is popped but the program doesn't end as the setTimeout is still not executed.
-> The setTimeout() was being executed in the background i.e a 0 delay was given and the passed function is immediately pushed into the task queue.
-> The function in the task queue which is foo(i+1) is pushed into empty call stack by event loop and then it starts its execution.
-> Then it again goes through all the steps from step 3.

Like that it just keeps running and will never fill the call stack. All thanks to the setTimeout() API provided by the browser.

Timeout Sort !!!

You would have heard of bubble sort, insertion sort, selection sort etc but do you know about timeout sort? Presenting a sort function which will obviously timeout on bigger numbers.

Fun aside, let us try sorting a few numbers using the setTimeout() API.

let arr = [10, 100, 500, 20, 35];

arr.forEach(item => {
  setTimeout(() => console.log(item), item);
})

// 10 20 35 100 500
//Credits: https://dev.to/karataev

How does it work? Let us see the execution flow again.

-> GEC is created as always. -> For every element in the array arr, the setTimeout function is called, pushed onto the stack and then offloaded to the browser one by one. -> Now these setTimeouts have 10, 100, 500, 20, 35 milliseconds of delay. -> So, once the shortest timer i.e 10 milliseconds ends, it is pushed to the task queue and event loop then pushes it to the call stack and 10 is logged. -> On the same lines, 20, 35, 100, 500 are logged in the order of delay end. -> Hence, we get a sorted output.

Isn't that cool?

Conclusion

So, that was a deep dive into the working of Javascript up to the task queue. There are two more major queues called the render queue and the microtask queue. I will have a part 2 of this blog with these two queues and a few more concepts as this blog is already too long.

By the way, if you got some value and liked the blog, do give it a thumbs up, it motivates a lot!!

And do follow me on Twitter @saai_tejaa

Let us meet again in the next blog!!!

Part 2: How Does JavaScript Even Work? (Part 2)

 
Share this