Asynchronous programming in javaScript
A couple of years ago when i was primarily working on/using java for development, came across Node.js and when i started reading the documentation Node.js is single threaded and is primarily suitable for IO heavy applications(like network calls and stuff), this particular sentence caught my eye. Until then i was under the assumption that if we wanted to make something faster, we just spawn multiple threads/processes with proper locks in place. Now i got confused with this single-threaded thingy. Thereafter started googling about how this is achieved and this writeup is about my findings.
A runtime which is very close or based on Node.js is the v8 engine which browsers like Google chrome/chromium are built on. If we were to grep for event-loop in the entire source code of v8, there won’t be any hits for our search. So where exactly this asynchronous/deferred functionality which is based on the event loop is coming from ?
We all know that JavaScript like other languages has a call stack, which contains the current state of the program. So if the runtime is single threaded there should be a single call stack and an heap associated which contains all the variables which are allocated. Let’s understand this with an example
// the following functions are just for illustration purpose only!
function square(x){
return x * x;
}
function printSquare(x){
let squaredX = square(x);
console.log(x + " squared is " + square(x))
}
printSquare(4);
Given that we have the above program which prints square of a number, during execution the state transition of the call stack will be as follows
Let’s now focus on a method which defers another method’s execution until a specific amount of time
console.log("Hi");
setTimeout(function cb(){
console.log("there");
}, 5000);
console.log("enlightentech.");
the corresponding state transition of the call stack will be as follows Wait a second, what is going on here? After approximately 5s from how did the console.log(“there”) statement end up on the call stack ?
Turns out that the runtime alongside this so called call stack, gives us some web apis which are threads themselves and execution of those web apis are tracked on a separately. Some of these apis are
- setTimeout
- DOM(document)
- ajax(XmlHttpRequest)
Upon completion of these invocations the callback function(which is supposed to be executed asynchronously) will be pushed on to another queue called the task queue. Complete visualization of the entities which are involved is something similar to the following diagram
Thus in the above example with the setTimeout, when the runtime encounters the setTimeout statement, the webApi gets invoked which executes in a separate thread and the runtime continues execution of the next statement, henceforth executing console.log(“enlightentech.”);. Another point to note is that all these web apis take a callback function as a parameter. In the above example, the callback function is executing console.log(“there”). But this does not mean that the call back function is also executed in the web api’s context, these call back functions are executed after the execution of the web apis.
SetTimeout in our example example is gonna wait for 5s once this time is passed, our callback function gets pushed to the task queue.
Alright, now we found that the callback does not get executed in the web api’s context. Where exactly does it get executed then ?
Meet the event loop
As shown in the above diagram, an event loop is an entity which keeps polling for functions in the task queue, and if the call stack is empty, it takes the call back function and pushes it on to the call stack. Once the call back lands on the call stack, its instructions are interpreted. Complete state transition of the setTimeout example will be as follows This is how the event loop helps us in performing asynchronous tasks in JavaScript.
Some concepts to keep in mind are
- For rendering the contents on the screen, the browser runtime(v8 etc), use a render function which also keeps polling for empty call stack so that it can re render the contents of the page, but if we have lots and lots of method invocations pushed on to the call stack or if our task queue is full of callbacks, the runtime will not be able to render animations properly. A golden rule of thumb is that we have to keep the call stack and the task queue as clean as possible(without pushing unnecessary instructions).
-
Let’s say that we have an instruction as follows
function foo(){ setTimeout(function bar(){ console.log("Hey there, please execute immediately!"); },0); }
even though our timer is set as 0, it is not guaranteed that our callback will get executed immediately, because the execution of the callback function depends on the contents of the task queue and the call stack, hence the time which you specify here as the argument is not the absolute time, it might take more than the specified time to execute.