Let me understand how JavaScript works under the hood
İnanç Akduvan
Posted on September 28, 2022
Did you ever wonder what's going on behind the scenes when you run a very small piece of code in JavaScript? I actually didn't for a long time.
I wanna understand it with you right now. Let's try to discover together! Hop on the bus, starting! 🚎
I offer to write a very very small code block and try to understand its story:
const totalApples = 10;
const totalBananas = 5;
function getTotalFruits() {
return totalApples + totalBananas;
}
const totalFruits = getTotalFruits();
Cool, we all know that result is 15 here. So easy. But what happened under the hood?
Before we start, keep in mind that JavaScript is single threaded and synchronous language:
- It can only execute one thing at a time.
- It executes them line by line from top to bottom.
FIRST OF ALL, Global Execution Context is created by JavaScript Engine.
JavaScript Engine
Javascript Engine is a program that executes the javascript code.
Execution Context
Imagine that: Execution context is a two-sided place where JS code is declared and executed. Imagine a board which has two sides.
On the right side (Memory), all variables and functions are declared.
On the left side (Thread of Execution), code is executed line by line.
There are 2 types of Execution Context:
Global Execution Context (GEC)
Whenever JS Engine executes a script, it creates a global execution context which all global code handled.
Function Execution Context (FEC)
Whenever a function is called, it creates a function execution context which has its own local memory and thread of execution.
LET'S TRY TO VISUALIZE:
Remember our code:
// Step 1
const totalApples = 10;
const totalBananas = 5;
// Step 2
function getTotalFruits() {
return totalApples + totalBananas;
}
// Step 3
const totalFruits = getTotalFruits();
Script executed, and Global Execution Context created:
Step 1:
Declare totalApples and totalBananas variables in the memory.
Step 2:
Declare getTotalFruits function in the memory.
Did you realized you can also save a function in the memory? Let's go on.
Step 3 (Part 1):
Declare totalFruits variable in the memory. But do I know value of this variable? NO! We cannot save commands in the memory. So it will be saved as uninitialized at the moment. See it:
Step 3 (Part 2):
Time to execute our function on Thread of Execution. AND FOR THE FIRST TIME HERE, Function Execution Context comes into our live. Here it is:
Is that all? NO my friend! Something is missing here. A new term reveals in this step. Whenever you call a function, it pops in Call Stack.
Call stack
Call Stack is where the JavaScript Engine keeps track of where your code is in the execution.
You can basically see which functions is running right now in the Call Stack.
🎤 Imagine that you are a singer. Announcer is calling your name. You are coming and singing your song. When the song is over, you are getting off the stage. Stage is Call Stack here. 🎤
When a function is called, it pops in Call Stack. And it pops out when it has done its job.
Global Execution is running at the bottom as default in Call Stack.
Function called:
Function finished its job:
Note that: There is ONLY ONE Call Stack since JavaScript is a single threaded language.
HUH! I guess it is done, right? Please warn me, if I do anything wrong. Let's keep up the work.
It looks okay like this, but, what if we add some complexity to our function?
Let's not be afraid and do it!
It looks very clean when it goes synchronously. But what will happen when some functions are asynchronous?
We can basically do it with setTimeout:
const totalApples = 10;
const totalBananas = 5;
function getTotalFruits() {
return totalApples + totalBananas;
}
function runTimer() {
console.log('timeout run!')
}
const totalFruits = getTotalFruits();
setTimeout(runTimer, 0);
console.log(totalFruits);
So guys, here is interesting. New terms reveal here again.
Did you know that JavaScript doesn't have a timer? Also console?
Everybody sometimes needs a support, so does Javascript. WEB APIs help JavaScript here.
Web APIs
Web APIs are generally used with JavaScript and add some power to our functionality. Timer, Console, Network requests and many other things are Web APIs' skills, not Javascript.
JavaScript just communicates with browser via some labels like setTimeout.
Let's Visualize
Assume that, it is setTimeout's turn in the Thread of Execution and let's try to visualize this step:
hmm, weird things are happening. Let me try to summarize it.
Please some attention now:
Functions and variables saved in memory as usual. They are executed in the thread. And when it is setTimeout's turn, JavaScript didn't send setTimeout's callback (runTimer) into Call Stack. Instead, it communicated with Web Browser and sent a message like:
''Hey buddy 👋 Can you start a timer for me? And when timer is completed, here is your callback to run (runTimer).''
As you see, after 0ms, timer is completed, callback is ready to run BUT guys, Thread of Execution never stops, exactly like life.
In the time when Web Browser handles timer, functions kept executing in the thread and even if callback(runTimer) is ready to be executed, Call Stack is not empty anymore! And remember: There can be only one execution at a time. BUT I HAVE TWO FUNCTIONS TO RUN IN MY HANDS.
What is gonna happen? Is JavaScript broken?!?!!?
Please no, we need it.Or will JavaScript run my functions in a random order?
That would be very unpredictable and disaster.
There should be a better solution.
🚨 NEW TERM ALERT 🚨
Our callback is waiting in Callback Queue.
Callback Queue
Callback Queue is kind of a waiting room for callbacks which are ready to be executed. Callback is waiting here for the Call Stack to empty. Whenever is Call Stack is empty, our callback pops in Call Stack and do its job.
Event Loop
Event Loops basically checks Call Stack continuously, and when Call Stack is empty, let there know about it. And first callback in the queue pops in Call Stack.
Let's add some visuals including Callback Queue and Event Loop
Oh guys, victory! 🏆
Just a little question; What if current execution continues for 1000 seconds or whatever while our callback is waiting in Callback Queue? Is it gonna wait forever?
Sounds unbelievable but yes! Whatever the cost, it is gonna wait until Call Stack is empty. No permission to run unless Call Stack is empty.
Okay! Adventure of this code block completed here, right? But I feel like we can add some more complexity here.
PLEASE DON'T LEAVE ME! I PROMISE THIS IS LAST SECTION. You will not regret, be patient!
What would happen if I need to fetch data from server?
Let's fetch some data:
const totalApples = 10;
const totalBananas = 5;
function getTotalFruits() {
return totalApples + totalBananas;
}
function runTimer() {
console.log('timeout run!')
}
function showData(data) {
console.log(data)
}
const totalFruits = getTotalFruits();
setTimeout(runTimer, 0);
// Fetch some data
const fetchData = fetch('https://jsonplaceholder.typicode.com/todos/1');
fetchData.then(showData);
console.log(totalFruits);
Okay, when we fetch data, we will communicate with Web Browser again. Because we need its APIs here.
I will not mention about details of fetch API, just talk about what is going on in the Thread of Execution, Call Stack and Queue while fetching data.
Hmm, let's try to write down a timesheet about what happened here:
1) Functions and variables saved in the memory, executed in the thread blablabla...
2) setTimeout communicated with Web Browser's timer.
3) setTimeout's callback (runTimer) is ready and waiting in Callback Queue after 0ms. [0ms]
4) fetch is doing two jobs:
a) Communicated with Web Browser's network feature.
b) Created a special Promise Object in the memory via Javascript Engine.
5) fetch's callback (showData) is ready with data to run after 200ms BUT we don't know yet where it is waiting right now. We'll see. [200ms]
6) While all those are happening, console.log(15) popped in Call Stack. AND assume that: it has taken 250ms to console and after 250ms it will pop out from Call Stack. (I know it wouldn't take that time in real life but just an assumption to understand the story). [250ms]
So, console.log(15) is running in the Call Stack, runTimer is waiting in Callback Queue. Where is showData waiting?
Based on our current knowledge, showData should be waiting right behind runTimer in the Callback Queue, right?
No my friend, it is not.
For the last time,🚨New Term Alert🚨:
There is an another kind of queue here besides Callback Queue.
Microtask Queue
Whenever a callback is related to Promise Object in JavaScript Engine, it will be waiting in Microtask Queue such as callbacks of fetch functions.
Note that: Microtask Queue has a privilege over Callback Queue.
First, callbacks in Microtask Queue will pop in Call Stack. After they are done, callbacks in Callback Queue will start to pop in Call Stack.
In our scenario, firstly, showData will run because it is waiting in Microtask Queue. And then, runTimer which is waiting in Callback Queue will run.
Then, final result looks like:
What an adventure! 😓 just a fucking piece of code, crazy.
Our journey ends here, guys. 🚎 thank you for joining me!
Stay in touch with me for more adventures 👇
Follow me on:
Github: https://github.com/inancakduvan/
Twitter: https://twitter.com/InancAkduvan
Thank you for coming with me until end of the reading! 🙂
Posted on September 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024