Whenever a JavaScript code is run on the browser, many elements are in play. A JavaScript Runtime contains -
- a JavaScript Engine which includes a Heap (memory) and a call stack.
- a collection of WebAPIs provided by the browser.
- an Event Loop.
- a Task Queue.
- and a Microtask Queue.
Call Stack
When we say JavaScript is a single threaded language, we are saying that the JavaScript Runtime has got a single Call Stack which is capable of running one task at a time.
Whenever execution of any script starts, JavaScript runtime goes line by line looking for things to push onto the call stack.
An an example, when console.log("My name is Sanjeet")
is encountered -
- an execution context is created and pushed onto the call stack.
- line is executed
- context is popped out of the call stack.
Whenever a function call is encountered, a new execution context is pushed onto the call stack for that function’s code.
So, if a computationally heavy synchronous operation is being carried out, the call stack cannot execute any other piece of code as it is blocked.
That’s where Web APIs come into picture!
Web APIs
Browser has provided a set of Web APIs to cater to different utilities like -
- a console.
- a timer (setTimeout, setInterval).
- HTML DOM APIs.
- fetch utility to fetch data asynchronously.
- Web Storage API.
- File API.
- Performance API.
and so much more…
Browser has provided these APIs and features to solve our usual problems like fetching some data, setting a timer, accessing our DOM, e.t.c. in a separate execution thread which does NOT block the Call Stack.
Even though the actual operations are performed by WebAPIs, their registrations (handing them out to WebAPIs), are pushed onto the Call Stack, and then popped out.
Browser exposes these APIs in 2 types -
Callback based APIs
One example of callback based API is the Geolocation API.
to which, we can provide a successCallback and an errorCallback which will get executed on success and error respectively.
One this API is encountered in the code -
- it’s registration is pushed onto the call stack.
- it’s handed over to the browser to run this API separately.
Once, it is successful, the registered callbacks are then pushed onto Task Queue or the Callback Queue rather than being pushed directly onto Call Stack.
Now, the concept of Event Loop comes into picture, wherein, the Event Loop tries to find a moment where Call Stack is empty and is ready to take some operation.
Event Loop then pushes the callback or task from the Task Queue to the Call Stack, where it’s finally executed.
One important note
One more such callback based APIs is setTimeout. The same flow happens with setTimeout (and setInterval) as well. So it’s important to note that the delay specified in the API is NOT the delay to execution.
Rather it’s the delay till it gets put into Task Queue.
Promise based APIs
Here, a special queue is used that goes by the name Microtask Queue, which is dedicated to
- then, catch and finally promise callbacks.
- body of execution after await on a promise.
- queueMicrotask callbacks.
- and MutationObserver callbacks.
One more special thing to note - the Event Loop, will prioritize Microtasks over Tasks in the Task Queue to send over to call stack for execution.
One more important note
Such microtasks, can often lead to other microtasks. If this scenario occurs, the Event loop just prioritizes the incoming Microtasks, never keeping the call stack empty for execution of Tasks, and such kind of scenario is called Task Starvation.
Promisifying callbacks
Normal Callbacks or Tasks can be promisifyed so that they are prioritized by the Event Loop by using the Promise constructor.
A Simple Exercise
Let’s take an example code.
The output to the above code will be -
5 1 3 4 2