JavaScript Execution – III

To read the first part of this series click here.

Until now, we have seen how JavaScript executes the code line-by-line. In this post, we will talk about how JavaScript engine works on a high level [the important parts].

Let’s start with the first and most important component of the JavaScript engine, i.e. call stack. The call stack is also the execution context, which means the current execution of the code is stored in the call stack. In phase 2 of the process of code execution, we have been following, we have seen the step ‘execution’. Execution is where call stack comes into the picture.

When the execution of a function starts, it is pushed to the stack.
When the function returns, it is popped from the call stack.

// Example 1.
1.  function foo () {
2.    // do something.
3.    foo2 ();
4.  }
5.  function foo2 () {
6.    // do something
7.    foo3();
8.  }
9.  function foo3 () {
10.   // do something
11.  }
12.  foo ();

According the steps we have seen already, we know that the first ‘execution’ step will take place at line no 12. And since we are executing this whole code [in the global context], let’s say it’s the main function which is executing this example. So, let’s put the main() function in the call stack [if it is not making sense, skip for now, you should be able to get it once you move further].

Here is how our call stack will look once the execution of this example starts,

// main is executing.
|         |
-----------
|         |
-----------
|         |
-----------
|         |
-----------
| main () |
-----------

When our first execution happens, at line no 12 of the above example, foo()is pushed into the stack, and it will remain there until it does not return.

// push foo()
|         |
-----------
|         |
-----------
|         |
-----------
| foo ()  |
-----------
| main () |
-----------

foo then calls the function foo2. For now, let’s not ignore what these functions are doing. If they make another function call, they’ll be added to stack just like we are doing now.

// push foo2()
|         |
-----------
|         |
-----------
| foo2()  |
-----------
| foo ()  |
-----------
| main () |
-----------

foo2 next calls foo3,

// push foo3()
|         |
-----------
| foo3 () |
-----------
| foo2()  |
-----------
| foo ()  |
-----------
| main () |
-----------

Once foo3 returns [every JavaScript function returns something or undefined], it’ll be popped out from the stack.

Then foo2 will return.

Next, foo will return.

In the end, main will also return indicating end of the execution and, call stack will become empty.

Blocking code, what’s that?

This was all very simple. When execution starts, push into stack, when execution ends, pop from call stack. JavaScrip is a single threaded language. As we already saw in the above example, it has only one call stack, so it has only one way/thread of executing code.

Let’s say in the above example, functionfoo2 requires5 seconds to complete, i.e. it has to do some work and it can return only after 5s. As we know, once the functions return, they are popped from the stack. And, in a stack, there is only one way to remove an item, i.e. from the top.

It means foo2 will be in the call stack for 5 seconds, and subsequently, foo and main will also be there for at least 5 seconds. For this whole time, the call stack will remain busy.

When it comes to web browsers and a webpage, there is another component called render queue. The browser tries to repaint the screen at a rate 60fps [generally]. Which means for each frame it gets 16.6 milliseconds. But it cannot do so when the call stack is not empty. 

This is the reason we see a lag or freezing of a webpage when we run a heavy JavaScript task.

Related: DOM and The Web Page

So in the example, we discussed right now, for 10 seconds, the webpage will be frozen. You will not be able to click on a button, or select text or do anything.

Make it async

An async or asynchronous call is something which does not block the call stack. Since JavaScript is synchronous and single-threaded, it takes helpfrom the Web APIs to exhibit asynchronous behaviour.

A callback function is a function which is called by another function based on certain condition.

Example:

All XHR calls made by the JavaScript takes help of Web API to complete the network request. In the XHR call, we pass a callback function, which executes when the XHR call completes/fails.

setTimeout is another such Web API. It accepts a function [callback function] which should be executed after a given amount of time [in milliseconds].

Async and the Call stack

When the call stack encounters any of such async function calls, it moves the callback function to the Web API component where it waits for its relevant condition to be fulfilled. The call stack executes the async function [setTimeout for example] by pushing the callback to Web API component, which means, this bit of execution gets completed, and the call stack becomes free for next execution. Let’s see an example:

// Example 1
1. console.log (1);
2. setTimeout (function () {
3.   console.log (2);
4. }, 5000);
5. console.log (3);

The call stack for this execution will look like this,

// after Line 1

// call stack
2.
1. console.log (1);  // executes immediately and popped out
0. main()

// Web API
EMPTY

// after Line 2

// call stack
2. 
1. setTimeout (cb, 5000); 
0. main ();

// Web API
EMPTY

// setTimeout executes immediately, cb moves to Web API and waits for 5000 milliseconds.

// call stack after line 2
2.
1.
0. main ();

// Web API
cb, waiting for 5000 milliseconds

// call stack after line 3
2.
1. console.log (3);  // executes immediately and popped out
0. main ();

// Web API
cb, still waiting (4.9 seconds left, e.g.).

As soon as Line 3 is executed, main is also popped out. The call stack is empty now, while it waits for the next set of instructions.

Event Loop and Callback Queue

The callback queue receives the functions from the Web APIs.

The Event Loop runs when the call stack is empty and removes the first element of the callback queue and pushes into the call stack.

So in this example, after 5 seconds, the callback function to print number 2, will be pushed to the callback queue. And it is clear that the call stack is empty, so the event loop will pull the callback function to the call stack and it will be executed immediately.

A more appropriate example of this would be, doing a network request (XHR call) and waiting for the response. The execution should not stop while a network request is in progress because it will block the call stack for that amount of time.

The amount of time given in the setTimeoutfunctiondoes not guarantee that the code will execute after that much time. It depends on the availability of the call stack. The event loop will only pull callback functions from the queue when the call stack is empty. So if there were some other blocking code which could require 10 s, the callback function with a timeout of 5 s will be executed after 10 s. Because, when the callback function is ready to be executed, the call stack is not.