visit
Today, we will demystify the JavaScript event loop. All those fancy terms like Callstack and Callback queue may sound complicated, but don't worry! I recall how tough it was when I initially started learning about it three years ago.
Hey, just a quick heads-up: you can actually spawn a Web Worker to run computations in parallel! It's pretty neat, but keep in mind that it has specific use cases, and there's one important thing to note: you won't be able to access the DOM from within the Web Worker. Just something to be aware of 🙂
console.log("A");
setTimeout(function() {
console.log("B");
}, 0);
console.log("C");
Notice how I scheduled a timer with setTimeout
, but I'm saying it should be called 0 milliseconds in the future.
Will the output be: A B C or A C B?
A, C, and then B are the answer.
console.log("A");
console.log("B");
Next, the JavaScript interpreter adds the first line to the call stack. So, at this moment, the call stack holds console.log("A")
.
The console.log("A")
statement is executed, which prints "A"
to the console. After that, this instruction is removed from the call stack, leaving it empty again.
Then, console.log("B")
is added to the call stack.
This line is executed, resulting in "B"
being logged to the console. Once again, this instruction is removed from the call stack, leaving it empty.
(It helps you visualize this)
Let's put the Web APIs and callback queue aside for a moment. Simply click on Save & run (I've already filled in the code for you), and take a peek at the Call stack.
Keep in mind that I won't use ES2015 since the loupe
tool doesn't support it since it was written over 7 years ago. However, the concepts still apply to arrow functions.
function init() {
console.log("A");
console.log("B");
}
init();
The function init()
is defined but not called, so it doesn't go into the call stack. The call stack stores the instructions that need to be executed.
When the execution encounters the call to init()
, it is added to the call stack because it can be executed.
At this point, init()
remains in the call stack, and the JavaScript interpreter starts reading the code inside the function line by line.
The line console.log("A")
is added to the call stack. Now, the call stack contains both init()
and console.log("A")
.
The console.log("A")
line is executed, which logs "A"
to the console. Then, console.log("A")
is removed from the call stack. So, the call stack now only contains init()
.
The next line, console.log("B")
, is executed. It logs "B"
to the console, and then console.log("B")
is removed from the call stack. Again, the call stack only contains init()
.
Since there are no more lines to execute inside the init()
function, init()
is removed from the call stack, and the call stack becomes empty once again.
When I introduce setTimeout
or other Web APIs, things can get a little trickier. These Web APIs work differently because of how JavaScript is designed and how it interacts with the visual part of the Browser (like the DOM, user interactions, network requests, and more).
So, instead of functions being executed immediately and added to the call stack, there is an intermediate step when using Web APIs like setTimeout
. Let's see an example below:
setTimeout(function logA() {
console.log("A");
}, 5_000);
I named the function logA
so that when we visualize this example, it will appear as logA
instead of being anonymous.
Then the code setTimeout(function logA() {...}, 5_000);
is added to the call stack.
After approximately 5 seconds, the timer elapses, and the browser notifies JavaScript that it's time to call the logA
function. However, the function cannot be directly added to the call stack.
Instead, it is added to the callback queue, which is a separate "array" that stores callbacks from WebAPIs like setTimeout
.
Currently, the call stack is still empty, but the callback queue contains logA()
.
Therefore, logA()
is moved from the callback queue to the call stack.
logA()
is executed, which adds console.log("A")
to the call stack, executes it, and then removes it.
Now that we understand the call stack and the callback queue, let's dive into the event loop, which is a straightforward part of the equation.
The event loop performs a task: it constantly checks if there are any items in the callback queue and sends them to the call stack, but only when the call stack is not occupied.
Observe how the callback queue accumulates three instructions - logA
, logB
, and logC
. Then, the event loop (depicted as an orange circle with arrows) forwards these tasks one by one, but only when the call stack is free.
console.log("A");
setTimeout(function logB() {
console.log("B");
}, 0);
console.log("C");
The setTimeout
call goes through the callback queue, so it runs after console.log("C")
. That's why you'll see A, C, and then B in the console. Give it a try yourself on .
When you use setTimeout(..., 0)
, it won't actually happen instantly in 0 milliseconds. Instead, there will be a few milliseconds that pass before the code executes.
This delay occurs because the code needs to go through the browser to schedule a timer for 0 milliseconds, then it enters the callback queue and waits for the call stack to be empty before running.
By the way, Philip Roberts is also the creator of the loupe
tool that you've been using in this post.
And guess what? There's a newer version of this topic available, which covers other similar APIs. It's called In the Loop by Jake Archibald. I also recommend checking it out. I hope you enjoy these videos and find them valuable for your career 😃