This is the first part of the blog series on JavaScript "Sync to Async" which explains how synchronous code execution works in JavaScript. And in the upcoming part, we will continue to advance our understanding by reading various features which make javascript asynchronous.
Let's start by picking up the basic features of the JavaScript engine.
JavaScript code is executed by the JavaScript engine which is an integral part of web browsers. Although JavaScript engines vary from browser to browser, they implement the same basic concept under the hood. For example, V8 is used in Chrome and Node, SpiderMonkey in Firefox, and so on.
JavaScript engines convert JavaScript code into bytecode, which is a form of machine-readable code. Call Stack and Memory Heap are the two primary components of JavaScript engines. There are several other aspects of JavaScript engines that I will cover in a separate post. For now, a basic understanding is all that is needed.
The image below provides a high-level overview of the components that make up a JavaScript engine (V8) and how it functions.
Principles of JavaScript :
Thread of execution:
JavaScript is a single-threaded language, which means that it can only perform one task at a time.
The thread of execution moves through the code line by line, executing each line of code. When it encounters declarators such as var, let, const, function, etc.. it stores the data in the computer's memory to be used later in the code.
Variable Environment:
The variable environment is the space allocated in memory by the javascript engine to store the data while executing the code.
In a variable environment, JavaScript engines have two places to store data: the call stack and the memory heap. Javascript, as we all know, has two data types: primitive and reference. Data is saved in either the call stack memory or the heap memory, depending on the data type. The stack holds both primitive data and references. The data referred to by stack references is saved in heap memory.
Let's take a closer look at this with the help of the code and figure provided below.
// primirive type
const number = 7;
const name = 'john';
// reference type
const person = {
id: 1,
name: 'john',
age: 22,
}
function getAge(person){
return person.age;
}
const newPerson = person;
const age = getAge(person);
As shown in the image below, the above code has three primitive data types number, name, and age
, whose data is directly stored in the call stack. There are also three reference data types person, getAge, and newPerson
, whose references are stored in the call stack and point to their respective data stored in the heap.
Note: The above image is a gif to view it from the start please refresh, I have kept it in a single loop to avoid confusion.
Execution Context:
Execution context is nothing but an environment to execute the code and store the data. When both the thread of execution and the variable environment are put together an execution context is formed, both of these contribute to two phases of the execution context: memory creation by the variable environment and code execution by the thread of execution.
There are two kinds of execution contexts in JavaScript:
To begin the execution of any JavaScript file, a global execution context will be created and placed onto the call stack by default. The global execution context is used to run any code that is present outside of the functions.
When we call a function, the function execution context is generated and placed onto the call stack. However, when the function has finished executing, it is popped off of the call stack.
Note: There is only one global execution context for each javascript file, but every function call has its own individual execution context, and an individual function may have multiple execution contexts.
Call Stack:
The call stack is an important part of the JavaScript engine that keeps track of currently executing functions by storing their execution contexts created during the function call as illustrated above in the variable environment.
when we call a function it gets pushed into the call stack and when the function is finished executing it gets popped out of the call stack.
The call stack follows the Last-In-First-Out (LIFO) principle which means that the items can be added or removed from the top of the stack only, and the item at the top indicates the function which is currently being executed by the javascript engine.
Note: these are just basic and simplified explanations there will be a lot more to each of the key concepts explained above, which we can explore and get into more depth.
Synchronous Execution of JavaScript:
Now that we are familiar with the essential concepts necessary for the synchronous execution of JavaScript. Let's put them together to have a better grasp of how JavaScript truly works.
To begin the execution of JavaScript, the global execution context will be created and placed into the call stack.
Following that, we have the thread of execution, which reads the code line by line, and the variable environment, which stores data while the code is running.
Next, we have the function execution context, once the thread encounters a function call, a new execution context is generated for that function and is pushed into the call stack, and after the function has finished running, it is popped out of the call stack.
we have the call stack to keep track of the execution contexts/function calls, to determine which code is currently being executed and the location of the thread.
Let us understand the above steps with an example given below.
const num = 3;
function operation1(input) {
const result = input + 3;
return result;
}
const output1 = operation1(num);
const output2 = operation1(7);
When this code runs, the following things will occur:
Creation of Global Execution Context;
As soon as we start executing this code, a
Global Execution Content
consisting of the thread of execution and variable environment is produced and pushed into the call stack.Memory allocation:
When the thread encounters declarators such as variables, functions, objects, and so on, it allocates memory space for them in the variable environment.
Note: The process described above is known as hoisting. Hoisting is the result of parsing javascript code before compiling it, there are also other concepts attached to it like a temporal dead zone, and how different declarators like let, var, functions, etc, are hoisted. I would recommend checking out how hoisting works, for a deeper understanding of how javascript is executed.
Code Execution:
Now, we move on to execute the code line by line using the thread of execution.
First, we have
const num = 3
which is executed and stored in the global memory.Next, we have a function declaration
operation1
. The entire function body is stored in the global memory. The code inside the function won’t be evaluated because the function has not been invoked.Next, we have
const output1 = operation1(num);
which is a function call, until the function call returns with some valueoutput1
will remain uninitialized.Creating Function Execution Contexts & Pushing into the Call Stack:
As the function "operation1" is invoked it creates a new execution context for that specific function call. Now the thread of execution enters the newly created execution context and local memory is created in the function's execution context to store the data specific to the function only.
When the function completes its execution, the result is assigned to
const output1 = 6
in the global execution context, and the function's local memory, alongside its execution context, is removed and garbage collected.Note: when I say the execution context is deleted and garbage collected when the function is done executing, there are some amazing concepts in JavaScript like closures, iterators and generators, etc.. that gives us the power to access the execution context even after the function is executed, amazing right !!!. The reason to point out this is that you will have this in mind and when you explore those concepts you will not be confused by the above explanation.
It is now your time to try executing the code const output2 = operation1(7); This is a function call identical to the previous line of code discussed above, meaning it repeats the cycle of points 4 and 5.
By studying the following two figures, you will be more equipped to fully understand all that has been said so far and visualize it.
In the below figure:
The global execution context, global memory and thread of execution are shown in white color which are initially created by the JS engine.
You can see the global declarations
num, operation1:→ƒ→, output1 and output2
are saved in global memory which is indicated in green color inside global memory.The function calls in
output1 and output2
which create their individual execution contexts are shown in orange color, you can observe the thread of execution entering inside the function's execution context and local memory being created. After the function is done executing the result gets returned to the global memory intooutput1: 6 & output2: 20
, and the thread of execution exits the function execution context.
The gif below depicts the entire cycle of the call stack for the above-mentioned example code.
Note: The above image is a gif to view it from the start please refresh, I have kept it in a single loop to avoid confusion.
Congratulations!!! 🎊, we now have a thorough understanding of how the javascript code executes synchronously. :)
Issues with the synchronous approach:
JavaScript is a single-threaded language, which means that at any given instance, JavaScript’s engine can only execute one statement or one line of code at a time, meaning the current task running must be completed before the next task begins. This is referred to as synchronous execution.
But let us imagine a scenario where our code has something to do outside the javascript engine like accessing a timer from the browser or making network requests etc. Now technically the code is not yet done executing so the thread cannot move to the next line but the thread also has nothing to do with such tasks so, It is sitting idle freezing the application until the task is done.
let me explain this more clearly with an example.
Code blocking with timer.
console.log("Me First");
// we will decalre a blocking function code which will block code for given amount of time passed as an parameter.
function RunMeLater(time){
// the thread will just wait hear for the given amount of time, blocking the execution for the given amount of time.
console.log(`I was executed after ${time} milliSeconds`)
}
RunMeLater(3000);
console.log(" why was thread idle and not executing me :( ");
let's try and understand what is happening with the above code with the help of the below-given illustration:
Task-1, the thread will read and execute
console.log("Me First");
.Task-2, we have a function declaration.
Task-3, we are calling the function
RunMeLater(3000);
with time as an argument, this function will block the thread from executing for the given time, while the timer is running.After the time has passed the thread inside the function will resume reading code and execute
console.log(`I was executed after 3000 milliSeconds`)
,which completes task-3.Task-4, the thread will read and execute
console.log(" why was thread idle and not executing me :( ");
Conclusion:
As seen in the steps listed above when javascript encounters a blocking code, nothing else can happen until that task is complete, which can cause the browser to appear frozen as the thread is idle and not executing any code, additionally, synchronous execution is incredibly time-consuming and slow because we can only run one line at a time and must wait for that line to complete execution before moving on. These are the main justifications for why we need asynchronous JavaScript.
Asynchronous JavaScript is the backbone of modern web development, as it accelerates and enhances the efficiency of our code execution, by allowing multiple things to happen at the same time.
Having said that, I am aware it was a long read, so congrats if you made it to the finish! I hope this information met your expectations. I'm Irfan Nawaz, and 'till next post!