I was trying out an example from a book to confirm how JavaScript event loop works, here is the code
const baz = () => console.log("baz");
const bar = () => console.log("bar");
const foo = () => {
console.log("foo");
setTimeout(bar, 0);
baz();
}
foo();
It is straightforward how setTimeout works here (executed out of order) output is
foo
baz
bar
What I don't understand is the order when I added one line
const baz = () => console.log("baz");
const bar = () => console.log("bar");
const foo = () => {
console.log("foo");
setTimeout(bar, 0);
baz();
}
setTimeout(baz, 0); // this somehow runs before foo() is finished
foo();
Output is
foo
baz
baz
bar
How come the second setTimeout rins before foo() is done?
Here is an explanation from an event-loop perspective.
You can visualize the call-stack, which is used to keep track of whereabouts we are in a program at a given point in time. When you call a function, we push it onto the stack, and when we return/complete a function, we pop it off the top of the stack. Initially, the stack is empty.
When you first run your code, your main "script" will be pushed onto the stack, and will be popped off the stack once the script has finished executing:
Stack:
------
- Main() // <-- indicates that we're in the main script
we then define a few functions baz
, bar
and foo
, and eventually reach our first function invocation, setTimeout(baz, 0)
, and so, we push it onto the stack:
Stack:
------
- setTimeout(baz, 0)
- Main()
setTimeout()
kicks off a web-api which after 0
ms enqueues your baz
callback onto the task queue. After setTimeout
has passed off its work to the web-api, its job is complete, and so it has finished its work and can be popped off the stack:
Stack:
------
- Main()
Task Queue: (Front <--- Back)
baz
It is the event loop's job to take tasks from the task queue and push them onto the stack when the stack is empty. Currently, the stack is not empty as we're still in the main script, so baz()
has not executed yet. The next function invocation we meet is foo()
, so we push this onto our stack:
Stack:
------
- foo()
- Main()
Task Queue: (Front <--- Back)
baz
Foo then calls the console object's log()
method, which is also pushed onto the stack:
Stack:
------
- log("foo")
- foo()
- Main()
Task Queue: (Front <--- Back)
baz
This logs "foo"
, and log()
is popped off of the stack as it has finished its work. We then continue stepping through the function foo. We now encounter a function call to setTimeout(bar, 0);
. This, much like the first function call, pushes setTimeout(bar, 0)
onto the stack. This spins off a web-api which adds bar
to the task queue. setTimeout(bar, 0)
is also complete once it has handed off its work to the web-api, so it also gets popped off the stack (see second and third ascii-diagrams for these steps), leaving us with:
Stack:
------
- foo()
- Main()
Task Queue: (Front <--- Back)
baz, bar
Finally, we arrive the last line in the function foo
which calls baz()
. This pushes baz()
onto the call stack, and then pushes log("baz")
to the top of the call stack, which logs "baz". So far, we have logged "foo" and then "baz". After baz has been logged log()
is popped off the stack and so is baz()
as it has finished.
Once the last line in foo()
is finished, we implicitly return, popping foo()
off the stack, leaving us with Main()
. Once we have returned from foo, our control/execution is returned back to the main script after where foo()
was invoked. As there are no more functions to call in our script, we pop Main()
off the stack, leaving us with:
Stack:
------
<EMPTY>
Task Queue: (Front <--- Back)
baz, bar
Now that the stack is empty, the event-loop can come in and handle baz
and bar
in the task-queue. First is takes baz
out of the queue and pushes it onto the stack, which then invokes log("baz")
, pushing log
onto the stack and then logging "baz". Once the log is complete, log
and baz
are popped off the stack leaving it empty again:
Stack:
------
<EMPTY>
Task Queue: (Front <--- Back)
bar
Now that the stack is empty again, the event-loop takes the first task from the queue (ie: bar
) and pushes in onto the stack. bar
then calls log("bar")
, which adds log("bar")
to the stack, as well as logs "bar" to the console. Once the logging is complete, log()
and bar()
are both popped off of the stack.
As a result, the output of your logs are printed in the following order (see bolded logs above):
"foo"
"baz"
"baz"
"bar"
Some good resources on the event loop and call stack can be found here, here and here.
You need to keep in mind that all the synchronous code will run first, then the asynchronous code. (setTimeout
will always queue up asynchronous actions, even if set with a timeout of 0.)
So, with this in mind the order of events is as follows:
baz()
foo
bar()
baz()
, outputting baz
Knowing the sync stuff runs first, we get foo
then baz
first.
Then our async events run, outputting baz
and bar
in turn.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With