Event Loop in JavaScript

JavaScript is a synchronous single threaded language.

It has only one call stack and can perform only one task at a time. This call stack is present inside the JavaScript Engine and all the code of JS is executed inside this call stack.

In order to understand further explanations, make sure you know these topics: execution context, callback functions

Whenever a JavaScript program is run, a global execution context is created and it is pushed into the call stack.

Further, whenever JS encounter a function call, a new execution context is created inside which the function is executed.

After a function has been executed completely, its execution context is deleted and removed from the call stack.

When there's nothing in the call stack for JavaScript to execute, the global execution context is also deleted and removed from the call stack.

So basically, whatever is provided to the call stack, it simply executes it. It doesn't wait for anything.

So how do we execute a particular portion of a code after sometime when JS waits for none?

Lets move back a little. All the code in JS is executed inside the call stack. The call stack is present inside the JS engine. Every browser has a JS engine.

Hierarchy: Browser -> JS Engine -> Call Stack

Web APIs

The browser also has a set of Web APIs that we can use in our JS program.

Example: setTimeout, DOM APIs, fetch(), localStorage, console, location, etc.

Now comes a shocker, all the examples mentioned above are part of the browser and not JavaScript.

We can access the web apis of the browser in JavaScript with the help of the global object, which in case of browsers is window.

window.console.log("Hello");

console.log("Hello twice");

Output

Hello

Hello twice

We can ommit `window` before using these Web APIs as its present in the global scope.

Internal Working of setTimeout

Let's look an example.

console.log("Start");

setTimeout(function() {

console.log("set timeout");

}, 1000);

console.log("End");

Output

Start

End

set timeout

Now let's understand how JavaScript executes this code.

Global Execution Context is created and inserted into the call stack.

JS uses the console web api and prints Start to the console.

JS encounters setTimeout, which calls the setTimeout web api. The setTimeout web api registers a callback and attaches a timer of 1000ms. This callback function is registered inside the web api's environment.

Time tide and JavaScript waits for none. JS moves to the next line and prints End to the console with the help of console web api.

JS is very fast and 1s has not passed yet. The call stack is empty and hence the global execution context is deleted and removed from the call stack.

When the timer expires, the callback function is pushed into the Callback Queue.

Now comes the elephant in the room. In order for JS to execute the callback function, the callback function has to be moved into the call stack. This is done by the event loop.

The event loop is responsible for monitoring the callstack, callback queue, and the microtask queue.

It is the responsibility of the event loop to check whether the call stack is empty and push the functions present in callback queue and microtask queue to the call stack once it's empty.

Coming back to the explanation, the callback function is pushed into the callback queue as soon as it's timer is finished.

Meanwhile, the event loop is constantly checking the call stack, callback and microtask queue.

It finds the callback function inside the callback queue.

Then it checks whether the call stack is empty or not, which in our case is empty.

The global execution context is recreated and put back into the call stack. Then the execution context of the callback function is created.

The event loop takes the callback function's execution context and puts it into the call stack.

JavaScript executes the callback function and set timeout is printed to the console, using the console web api.

After all of this, the execution context of the callback function is deleted and removed from the call stack.

The global execution context is also deleted and removed from the call stack.

Internal Working of eventListener

Let's have a look at a similar example.

const button = document.querySelector('button');

button.addEventListener('click', () => {

console.log("clicked");

});

The above code will print `clicked` to the console every time the user clicks a button on the webpage.

The global execution context is created and pushed into the call stack.

document. is a part of the collection of DOM APIs present in the browser.

JS calls this api which fetches the button from the dom and this is stored in the variable `button`.

addEventListener registers a callback function with an event of `click` inside the web api's environment.

Post this the call stack becomes empty, so the gec is deleted and removed from the call stack.

But the event handler will remain in the web api's environment unless we explicitly remove this event listener from the button or close the webpage.

Now whenever the user clicks on this button, the callback function of the event handler is pushed into the callback queue.

Now the event loop will catch this and check whether the call stack is empty or not, which in this case, it is.

The global execution context is recreated and pushed into the call stack, followed by the creation of the execution context of the event handler's callback function and it's execution context being pushed into the call stack.

JS calls the console web api and `clicked` is printed on the console.

Post this the callback function's execution context is deleted and removed from the call stack, followed by the gec being deleted and removed from the call stack.

It is important to note that the event loop is constantly checking the callback and the microtask queue in case anything is to be executed.

If the user clicks the button again, the entire process is repeated.

This is how JS handles eventListeners.

Need of callback/ microtask queue

Let's say the user clicks a button that has an event listener of click attached to it.

function closure() {

let count = 0;

const button = document.querySelector("button");

button.addEventListener("click", () => {

console.log(++count);

});

}

closure();

The above program basically prints the number of times the button has been clicked, each time we click the button. If the user clicks the button multiple times, multiple callback functions would be registered and sent to the callback queue so that they are sent to the call stack in the right order one at a time by the event loop.

If there were no such queues, there would be no way to synchronise the order in which they're sent to the call stack, which is crucial.

Also, JS has only one call stack and being a single threaded language, it can execute one thing at a time. So there's no point in sending all the callback functions at the same time to the call stack, in case you were wondering this.

Internal working of fetch

Let's look at an example

setTimeout(function() {

console.log("setTimeout");

}, 1000);

fetch("api_url").then(function() {

console.log("promise resolved");

});

--millions of lines of code

console.log("End");

Output

End

promise resolved

setTimeout

[Assume the million lines of code take more than a second to execute]

[Assume promise of fetch is resolved within 100ms]

setTimeout will be executed as explained in the previous example.

Similar to setTimeout, fetch will also register its callback function to the web api's environment.

Now we have two callbacks registered into the web api's environment.

setTimeout's callback function is waiting for the timer to expire, while fetch's callback function is waiting for the promise to get resolved.

Assume the promise returned by fetch gets resolved in less than 100ms.

Now post 100ms fetch's callback function is sent to the microtask queue.

The Microtask Queue is similar to the callback queue but it is of higher priority. This means if there is a function in the callback queue and concurrently there is also another function in the microtask queue, the function in the microtask queue is passed into the call stack on priority.

Now the question might arise, what goes inside the callback queue and what goes into the microtask queue?

All the callback functions that come through promises and mutation observer go into the microtask queue. All other callbacks are pushed to the callback queue.

Let's come back to the explanation.

Both the callbacks from fetch and setTimeout are present in the web api's environment. After 100ms (assumption) promise of fetch is resolved and the callback is pushed into the microtask queue.

Next JS will execute the millions of lines of code present in the code.

Meanwhile, 1000ms is up and the callback function of setTimeout is pushed into the callback queue.

But since the call stack is not empty till now, JS will keep on executing the millions of code lines.

After it's done executing, `End` is printed to the console through the console web api.

The global execution context is deleted and removed from the call stack.

Now the event loop has two callback functions to choose from, one in the microtask queue and the other in the callback queue.

Due to its higher priority, the callback from the microtask queue i.e., the fetch callback's execution context will be pushed to the call stack right after a new global execution context has been recreated and pushed to the call stack.

Now `promise resolved` is printed to the console through the console web api post which the callback's execution context is deleted and removed from the call stack. This is followed by the destruction and removal of the global execution context from the call stack.

Now again the call stack is empty, so the event loop picks up the setTimeout's callback and sends it to the call stack.

Again a global execution context is created and pushed into the call stack followed by the creation of execution context of setTimeout's callback, which is also pushed into the call stack.

`setTimeout` is printed to the console, thanks to the console web api. After this both the execution contexts are destroyed and removed from the call stack.

Hope you liked the explanation :D

Starvation of functions in Callback Queue

Think of a situation where there are 100s of functions in the microtask queue and a single function in the callback queue. When the event loop finds that the call stack is empty, it'll prioritize the microtask queue functions and only after all the functions in the microtask queue have been executed, the callback in the callback queue is sent to the call stack. This situation is called starvation of function(s) in callback queue. The function in the callback queue had to wait a long time before being sent to the call stack.