引用自:

Regular Event Loop

This shows the execution order given JavaScript’s Call Stack, Event Loop, and any asynchronous APIs provided in the JS execution environment (in this example; Web APIs in a Browser environment)


Given the code

1
2
3
setTimeout(() => { 
console.log('hi')
}, 1000)

The Call Stack, Event Loop, and Web APIs have the following relationship

1
2
3
4
5
6
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |

To start, everything is empty


1
2
3
4
5
6
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |

It starts executing the code, and pushes that fact onto the Call Stack (here named <global>)


1
2
3
4
5
6
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
> setTimeout(() => { | <global> | | | |
console.log('hi') | setTimeout | | | |
}, 1000) | | | | |
| | | | |

Then the first line is executed. This pushes the function execution as the second item onto the call stack.

Note that the Call Stack is a stack;

The last item pushed on is the first item popped off. Aka: Last In, First Out. (think; a stack of dishes)


1
2
3
4
5
6
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
> setTimeout(() => { | <global> | | | timeout, 1000 |
console.log('hi') | setTimeout | | | |
}, 1000) | | | | |
| | | | |

Executing setTimeout actually calls out to code that is not part of JS. It’s part of a Web API which the browser provides for us. There are a different set of APIs like this available in node.


1
2
3
4
5
6
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | | | timeout, 1000 |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |

setTimeout is then finished executing; it has offloaded its work to the Web API which will wait for the requested amount of time (1000ms).


1
2
3
4
5
6
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | timeout, 1000 |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |

As there are no more lines of JS to execute, the Call Stack is now empty.


1
2
3
4
5
6
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | function <-----timeout, 1000 |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |

Once the timeout has expired, the Web API lets JS know by adding code to the Event Loop. It doesn’t push onto the Call Stack directly as that could intefere with already executing code, and you’d end up in weird situations. The Event Loop is a Queue. The first item pushed on is the first item popped off. Aka: First In, First Out. (think; a queue for a movie)


1
2
3
4
5
6
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function <---function | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |

Whenever the Call Stack is empty, the JS execution environment occasionally checks to see if anything is Queued in the Event Loop. If it is, the first item is moved to the Call Stack for execution.


1
2
3
4
5
6
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function | | | |
> console.log('hi') | console.log | | | |
}, 1000) | | | | |
| | | | |

Executing the function results in console.log being called, also pushed onto the Call Stack.


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
> hi

Once finished executing, hi is printed, and console.log is removed from the Call Stack.


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
> hi

Finally, the function has no other commands to execute, so it too is taken off the Call Stack.

Our program has now finished execution.

End.

Starved Event Loop

Below is an example of how code running in the current Call Stack can prevent code on the Event Loop from being executed. aka; the Event Loop is starved.


Given the code

1
2
3
4
5
setTimeout(() => { 
console.log('bye')
}, 2)
someSlowFn()
console.log('hi')
1
2
3
4
5
6
7
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

To start, everything is empty


1
2
3
4
5
6
7
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | | | |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

It starts executing the code, and pushes that fact onto the Call Stack (here named <global>)


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
> setTimeout(() => { | <global> | | | |
console.log('bye')| setTimeout | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

setTimeout is pushed onto the Call Stack


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
> setTimeout(() => { | <global> | | | timeout, 2 |
console.log('bye')| setTimeout | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

setTimeout triggers the timeout Web API


1
2
3
4
5
6
7
      [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | | | timeout, 2 |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

setTimeout is then finished executing, while the Web API waits for the requested amount of time (2ms).


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | | | timeout, 2 |
console.log('bye')| someSlowFn | | | |
}, 2) | | | | |
> someSlowFn() | | | | |
console.log('hi') | | | | |

someSlowFn starts executing. Let’s pretend this takes around 300ms to complete. For that 300ms, JS can’t remove it from the Call Stack


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | function <-----timeout, 2 |
console.log('bye')| someSlowFn | | | |
}, 2) | | | | |
> someSlowFn() | | | | |
console.log('hi') | | | | |

Meanwhile, the timeout has expired, so the Web API lets JS know by adding code to the Event Loop. someSlowFn is still executing on the Call Stack, and cannot be interrupted, so the code to be executed by the timeout waits on the Event Loop for its turn.


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | function | | |
console.log('bye')| someSlowFn | | | |
}, 2) | | | | |
> someSlowFn() | | | | |
console.log('hi') | | | | |

Still waiting for someSlowFn to finish…


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | function | | |
console.log('bye')| | | | |
}, 2) | | | | |
> someSlowFn() | | | | |
console.log('hi') | | | | |

someSlowFn finally finished!


1
2
3
4
5
6
7
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | function | | |
console.log('bye')| console.log | | | |
}, 2) | | | | |
someSlowFn() | | | | |
> console.log('hi') | | | | |

The next line is executed, pushing console.log onto the Call Stack


1
2
3
4
5
6
7
8
9
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <global> | function | | |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
> console.log('hi') | | | | |

> hi

We see hi output on the console thanks to console.log


1
2
3
4
5
6
7
8
9
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | function | | |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

> hi

Nothing left to execute, so the special <global> is popped off the Call Stack.


1
2
3
4
5
6
7
8
9
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function <---function | | |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

> hi

This frees up the JS execution environment to check the Event Loop for any code which needs to be executed.


1
2
3
4
5
6
7
8
9
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function | | | |
> console.log('bye')| console.log | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

> hi

Executing the function results in console.log being called, also pushed onto the Call Stack.


1
2
3
4
5
6
7
8
9
10
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function | | | |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

> hi
> bye

Once finished executing, bye is printed, and console.log is removed from the Call Stack.

Notice that by this point, it is at least 300ms after the code originally requested the setTimeout. Meaning even though we asked for it to be executed after only 2ms, we still had to wait for the Call Stack to empty before the setTimeout code on the Event Loop could be executed

Note: Even if we didn’t have someSlowFn, setTimeout is clamped to 4ms asthe mimimum delay allowed in some cases


1
2
3
4
5
6
7
8
9
10
        [code]        |   [call stack]    | [Event Loop] | |   [Web APIs]  |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | |
console.log('bye')| | | | |
}, 2) | | | | |
someSlowFn() | | | | |
console.log('hi') | | | | |

> hi
> bye

Finally, there are no other commands to execute, so it too is taken off the Call Stack.

Our program has now finished execution.

End.

Note: It’s also possible to starve the event loop with Promises via the “Microtask queue”

Event Loop Explained

basic

Code Snippet 1 : Intrigue the mind

1
2
3
4
5
6
7
8
9
10
11
12
function main(){
console.log('A');
setTimeout(
function display(){ console.log('B'); }
,0);
console.log('C');
}
main();
// Output
// A
// C
// B

demo1

  1. The call to the main function is first pushed into the stack (as a frame). Then the browser pushes the first statement in the main function into the stack which is console.log(‘A’). This statement is executed and upon completion that frame is popped out. Alphabet A is displayed in the console.
  2. The next statement (setTimeout() with callback exec() and 0ms wait time) is pushed into the call stack and execution starts. setTimeout function uses a Browser API to delay a callback to the provided function. The frame (with setTimeout) is then popped out once the handover to browser is complete (for the timer).
  3. console.log(‘C’) is pushed to the stack while the timer runs in the browser for the callback to the exec() function. In this particular case, as the delay provided was 0ms, the callback will be added to the message queue as soon as the browser receives it (ideally).
  4. After the execution of the last statement in the main function, the main() frame is popped out of the call stack, thereby making it empty. For the browser to push any message from the queue to the call stack, the call stack has to be empty first. That is why even though the delay provided in the setTimeout() was 0 seconds, the callback to exec() has to wait till the execution of all the frames in the call stack is complete.
  5. Now the callback exec() is pushed into the call stack and executed. The alphabet C is display on the console. This is the event loop of javascript.

So the delay parameter in setTimeout(function, delayTime) does not stand for the precise time delay after which the function is executed. It stands for the minimum wait time after which at some point in time the function will be executed.

Code Snippet 2 : Deeper Understanding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function main(){
console.log('A');
setTimeout(
function exec(){ console.log('B'); }
, 0);
runWhileLoopForNSeconds(3);
console.log('C');
}
main();
function runWhileLoopForNSeconds(sec){
let start = Date.now(), now = start;
while (now - start < (sec*1000)) {
now = Date.now();
}
}
// Output
// A
// C
// B

demo2

  • The function runWhileLoopForNSeconds() does exactly what its name stands for. It constantly checks if the elapsed time from the time it was invoked is equal to the number of seconds provided as the argument to the function. The main point to remember is that while loop (like many others) is a blocking statement meaning its execution happens on the call stack and does not use the browser APIs. So it blocks all succeeding statements until it finishes execution.
  • So in the above code, even though setTimeout has a delay of 0s and the while loop runs for 3s, the exec() call back is stuck in the message queue. The while loop keeps on running on the call stack (single thread) until 3s has elapsed. And after the call stack becomes empty the callback exec() is moved to the call stack and executed.
  • So the delay argument in setTimeout() does not guarantee the start of execution after timer finishes the delay. It serves as a minimum time for the delay part.