If you are someone who has heard about the terms event loop, callback queue, concurrency model and call stack but doesn't really understand what they actually mean, this post is for you. Having said that, if you're an experienced developer, this post might help you to understand the internal working of the language and enable you to write more performant user interfaces.
The JS engine will initialize memory for the function declarations in the heap. When it reaches line 11, it will encounter a function execution and the JS runtime will push it into the Call Stack. The following steps demonstrate the state of call stack at each step -
Each entry in the Call Stack is called a stack frame. You might have already seen it when the browsers prints a stack trace on the occurrence of an error.
Assuming the file is named main.js, the following stack trace will be displayed in the console
Let's say we have a recursive function that calls itself infinite times. For example -
On execution, the function bar will get added to the call stack on each call until the call stack finally runs out of memory and throws an out of range error. This is infamously known as blowing the stack.
Let's talk async
Before we really dig into async code, we'll see how blocking or synchronous code effects our Call Stack. Take a look at the jQuery code below for example
The first line basically sets all the ajax requests to be synchronous. Thus, while we're are awaiting the response from the ajax requests, the Call Stack is blocked and our program will remain unresponsive for that time. If you would simulate the same behavior of the above code in a browser, all the other elements of the page will go into a blocked state i.e. you can not interact with them until they exit the blocked state. This is the reason why it is a bad practice to perform synchronous network requests or any other operation that requires large computation time.
The solution to this is pretty straightforward - asynchronous callbacks. You probably have already used asynchronous code in your program. APIs like setTimeout or xhr requests are asynchronous. But before we explore how they actually work, let us visualize how the Call Stack appears when it executes async code.
On execution ,when the program encounters a setTimeout, it queues it to be executed after a certain interval and then moves on to the next line. After the stack is empty (and the timeout is finished) the setTimeout callback magically gets pushed into the stack and is executed. We will see how this happens in the next section.
Concurrency model and the Event Loop
Apis like setTimeout, xhr etc are not present in the runtime but are rather provided by the Web Apis. When you call the setTimout function, it registers a timer function along with the callback. When the timer expires, it sends the callback to the event queue which is then pushed to the Call Stack by the Event Loop when the Call Stack is empty.
The Event Loop has a single job - it watches the Call Stack and the Callback Queue. When the Call Stack is empty, it takes the first event in the queue and pushes it to the stack which effectively runs it. Such an iteration is called a tick in the Event Loop. Each event is just a function callback.
A thing about setTimeout(..)
One of my earliest encounters with async code was this -
It initially seems that the timeout callback will execute almost immediately. However, it is important to note that the timer itself doesn't put the callback into the Callback Queue. When the timer expires, the environment puts the callback to the queue which is pushed to Call Stack when it's empty.
Thus, setTimeout doesn't make your callback to run after a specified time. Rather it just ensures the minimum time after which the callback executes. With a timeout of 0 seconds, the timer expires almost immediately and the callback is placed in the Callback Queue. But the Event Loop still has to wait until the stack is empty before it can push the callback in it. This means that we basically defer the execution of the callback until the stack is clear.
Understanding the environment in which your program runs can significantly increase your efficiency and effectiveness as a developer. It highlights the logic as to why a given program works the way it does. If you felt there was something missing or if you like the post, do let me know in the comments. You can also follow me on Github or Twitter where I share insights on programming and developement in general.
Over and out!