┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
THE EVENT LOOP
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
┌───────────────────────────────┐
│ poll │
┌─▶│ │──┐
│ └───────────────┬───────────────┘ │
│ │ tick
│ ┌───────────────▼───────────────┐ │
│ │ check │ │
│ │ │◀─┘
│ └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ close callbacks │
│ │ │
loop └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ timers │
│ │ │
│ └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ pending callbacks │
│ │ │
│ └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ idle, prepare │
└──│ │
└───────────────────────────────┘
The event loop (in Node.js) is an execution model where aspects of a script are executed in a cyclical manner according to a defined schedule.
It [event loop] is made up of a number of phases (as illustrated above). Each phase contains (1) a call stack, and (2) a callback queue. The call stack is where code is executed (on a LIFO basis), while the callback queue is where code is scheduled (on a FIFO basis) for later placement in the call stack for execution.
This callback queue can be sub-divided into 2 queues: a microTask queue and a macroTask queue. a micro-task (once scheduled) is a task that will be executed immediately after the current running script in the current phase, while a macro-task (once scheduled) is a task that will be executed in the next loop of said phase (after any micro-tasks in that phase).
The event loop runs in a cycle through all phases repeatedly until there is no more work to be done. Each cycle (through all the phases) can be referred to as a loop, while each complete invocation of scripts in a given queue can be referred to as a tick.
This tick will usually happen from one phase to another, but a tick can happen within a phase when both the microTask and macroTask queues are not empty e.g. when a Promise is resolved in the running script, its then
method adds items to the microTask queue.
When you write code (say in a mycode.js
file) and then invoke it (with node mycode.js
), this code will be executed using the event loop according to how it is written.
Here's an example script:
process.nextTick(function() {
console.log('next tick - 1 [scheduled from poll]');
});
console.log('poll phase - 1');
setImmediate(function() {
console.log('check phase - 1');
process.nextTick(function() {
console.log('next tick - 2 [scheduled from check]');
});
Promise.resolve()
.then(function() {
console.log(`check phase - 1.1 [microTask]`);
})
.then(function() {
console.log(`check phase - 1.2 [microTask]`);
})
.then(function() {
setTimeout(function() {
console.log('timers phase [scheduled from Promise in check]');
});
process.nextTick(function() {
console.log('next tick - 3 [scheduled from Promise in check]');
});
});
console.log('check phase - 2');
});
setTimeout(function() {
console.log('timers phase - 1');
setImmediate(function() {
console.log('check phase [scheduled from timers]');
});
Promise.resolve()
.then(function() {
console.log('timers phase - 1.1 [microTask]');
})
.then(function() {
console.log('timers phase - 1.2 [microTask]');
})
.then(function() {
setTimeout(function() {
console.log('timers phase [scheduled from Promise in timers]');
});
});
});
process.nextTick(function() {
console.log('next tick - 4 [scheduled from poll]');
});
console.log('poll phase - 2');
Copy (or type) this into a .js file, and invoke it with node
.
You should get the following output:
poll phase - 1
poll phase - 2
next tick - 1 [scheduled from poll]
next tick - 4 [scheduled from poll]
check phase - 1
check phase - 2
next tick - 2 [scheduled from check]
check phase - 1.1 [microTask]
check phase - 1.2 [microTask]
next tick - 3 [scheduled from Promise in check]
timers phase - 1
timers phase - 1.1 [microTask]
timers phase - 1.2 [microTask]
timers phase [scheduled from Promise in check]
check phase [scheduled from timers]
timers phase [scheduled from Promise in timers]
Note: Using Node.js version 16.15.0
Before the explanation, here are a few rules to remember:
setImmediate
schedules scripts to run in the next check phase of the event loop (in the macroTask queue)
setTimeout
schedules scripts to run in the next timers phase of the event loop (in the macroTask queue)
Process.nextTick
schedules scripts to run before the next tick i.e. either (1) after the current script has run but before the microTask queue has run [if said queue is not empty], or (2) before the event loop traverses from one phase to the next [if microTask queue is empty]
Promise.prototype.then
schedules scripts to run in the current microTask queue i.e. after the current script, but before scripts scheduled for the next phase
- The microTask queue is run before the macroTask queue
Here's the explanation in the form of a timeline of events:
A. FROM POLL PHASE (LOOP 1)
console.log('poll phase - 1')
and console.log('poll phase - 2')
are synchronous code and will run immediately in the current phase
console.log('next tick - 1 [scheduled from poll]')
and console.log('next tick - 4 [scheduled from poll]')
are scheduled by process.nextTick
to run before the next tick i.e. before the check phase (since there is nothing in microTask queue).
- The callback on
setImmediate
(Line 7) is scheduled to run in the check phase
- The callback on
setTimeout
(Line 33) is scheduled to run in the timers phase
B. BEFORE CHECK PHASE (LOOP 1)
5. console.log('next tick - 1 [scheduled from poll]')
and console.log('next tick - 4 [scheduled from poll]')
are executed
C. FROM CHECK PHASE (LOOP 1)
6. console.log('check phase - 1')
and console.log('check phase - 2')
[from callback previously scheduled by setImmediate
(Line 7)] are executed immediately as they are synchronous
7. console.log('next tick - 2 [scheduled from check]')
is scheduled by process.nextTick
8. The callbacks on Line 15, 18, and 21 are scheduled to run in the microTask queue.
9. console.log('next tick - 2 [scheduled from check]')
is executed (because this is before the next tick i.e. after the current script but before microTask queue)
10. The callbacks on Line 15 and 18 are executed (because the microTask is executed immediately after the running script)
11. The callback on Line 21 is executed and schedules (1) console.log('timers phase [scheduled from Promise in check]')
to run in the next timers phase, and (2) console.log('next tick - 3 [scheduled from Promise in check]')
to run before the next tick i.e. before traversal from current phase (check) to the next active phase (timers)
D. BEFORE TIMERS PHASE (LOOP 1)
12. console.log('next tick - 3 [scheduled from Promise in check]')
is executed
E. FROM TIMERS PHASE (LOOP 1)
13. console.log('timers phase - 1')
is executed
14. setImmediate
(Line 36) schedules its callback to be run in the next check phase
15. The Promise
(Line 40) schedules three callbacks to be run in the microTask queue
16. console.log('timers phase - 1.1 [microTask]')
and console.log('timers phase - 1.2 [microTask]')
are executed as scheduled in 15.
17. console.log('timers phase [scheduled from Promise in check]')
is executed. It was previously scheduled by setTimeout
(Line 22). It's running now (after the code in 16. above) because it is a macroTask (so it runs after the microTask queue has been run)
E. FROM NEXT CHECK PHASE (LOOP 2)
18. console.log('check phase [scheduled from timers]')
is executed. It was previously scheduled in the timers phase (of Loop 1) by setImmediate
(Line 36)
F. FROM NEXT TIMERS PHASE (LOOP 2)
19. console.log('timers phase [scheduled from Promise in timers]')
is executed. It was previously scheduled in the timers phase (of Loop 1) by setTimeout
(Line 48)
References