Javascript is a single threaded language, at least the majority of it. Things are executed in a certain order. But at times it does work in a non-blocking manner which sort of gives it an asynchronous appearance. But how does the Javascript engine handle all of the script, Browser APIs everything at once ? I can't do all of it at once, even I got like 10 hands.
Before explaining how things work I'll give a briefing of different parts involved in running Javascript.
Call Stack:
Javascript is single threaded, it means it has one call stack. Call Stack is a data structure which keeps track of function calls in our program. When ever we call a function for its execution, we are pushing it to the stack. It is popped out of the stack when the execution is completed.
Heap:
This is where all the memory allocation happens for your variables, that you have defined in your program.
Tasks
Tasks ensure that actions are happening sequentially. A task can be as simple as any function scheduled in the Javascript. Between the tasks, the browser may render updates. All the things mentioned here are generally Macro Tasks. Callbacks of such tasks are usually dealt by a data structure called Task Queue.
There is also something called Micro Task. Micro Tasks are useful to make something async without taking the penalty of a whole new task.
As per MDN:
A microtask is a short function which is executed after the function or program which created it exits and only if the JavaScript execution stack is empty, but before returning control to the event loop being used by the user agent to drive the script's execution environment.
With ES6, Promises have been introduced into Javascript, which along with Mutation Observer, use the microtask queue to run their callbacks.
Task Queue:
Task Queue is a JavaScript run-time messaging queue which handles task that is allocated by different Web APIs. This queue is dedicated to handle the Web API callbacks. The messages are processed once the call stack is clear.
Microtask Queue:
As mentioned above, Microtask queue handles callbacks with respect to Promises and Mutation Observer.
Event Loop:
It has responsibility to see weather the call-stack is empty and does the task queue contains pending task to process. If the call-stack is empty, it will push the task to the call-stack from the queue and the task gets processed.
Here is how the Event Loop handles different tasks:
First, each time a task exits, the event loop checks to see if the task is returning control to other JavaScript code. If not, it runs all of the microtasks in the microtask queue. The microtask queue is, then, processed multiple times per iteration of the event loop, including after handling events and other callbacks.
Second, if a microtask adds more microtasks to the queue by calling queueMicrotask(), those newly-added microtasks execute before the next task is run. That's because the event loop will keep calling microtasks until there are none left in the queue, even if more keep getting added.
All of it can be better illustrated by the code below:
console.log('start')
setTimeout(()=>console.log('Time-out 1'),0)
setTimeout(()=>console.log('Time-out 2'),0)
Promise.resolve().then(()=>console.log('Promise 1'))
Promise.resolve().then(()=>console.log('Promise 2'))
console.log('end')
The output on Chrome would be:
start
end
Promise 1
Promise 2
Time-out 1
Time-out 2
Now why is that so?
Firstly, the two script jobs are scheduled on to the Call Stack as the first task. The logging start is picked up from the task queue and executed.
Set Timeout is handled by the Browser as it is a Browser window object. The timer and everything is calculated by the Browser as waits for a given delay then schedules a new task for its callback in the Task Queue. This is same for both the Timeouts. So there are two Timeout tasks waiting in the Task queue. As logging script end is part of the first task, so setTimeout is logged in a separate task.
After that, the Promises are run and their callbacks are pushed to the Microtask queue. They're pushed to the queue because the first task isn't yet completed and microtasks callbacks can't be run when one task is in mid-execution.
Then the logging end as it is also part of the first task which is yet to be completed. Note that we have already two Promise callbacks pending in the microtask queue, thus as soon as there is no task in middle of completion, the event loop prioritizes the completion of the microtasks callbacks(over the timeouts) and pushes them to the Call Stack. This process continues until the Mirco Task queue is empty.
Then the rest of the task queue is pushed to the Call Stack.
I Hope the above process was clear.
However the output will vary from Browser to Browser. This is sort-of excusable, as promises come from ECMAScript rather than HTML. ECMAScript has the concept of "jobs" which are similar to microtasks, but the relationship isn't explicit aside from vague mailing list discussions. However, the general consensus is that promises should be part of the microtask queue, and for good reason.
Before concluding this write-up, a brief intro on Web-Workers.
Web-workers help to handle the intensive tasks, which would have blocked the UI. Web-workers help in making the best use of multi-threading.
But wait, Wasn’t Javascript a single-threaded language? The very first sentence of this writeup says so?
Well, I stand true to my words. This is because, the Web workers aren’t part of the Javascript, rather they’re a built in features of the browsers which are being accessed through Javascript. Web Workers are in-browser threads that can be used to execute JavaScript code without blocking the event loop.
Will be covering them in a separate article shortly.
Till then Happy Quarantine, Stay Safe and Keep Learning!
Top comments (0)