Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript Event Loop out of order execution

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?

like image 610
Ivo Avatar asked Jan 25 '23 15:01

Ivo


2 Answers

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 0ms 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.

like image 166
Nick Parsons Avatar answered Jan 30 '23 12:01

Nick Parsons


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:

  • register an asynchronous call to baz()
  • synchronously output foo
  • register an asynchronous call to bar()
  • synchronously call 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.

like image 40
Mitya Avatar answered Jan 30 '23 10:01

Mitya