For those JavaScript programmers, event loop is an important concept, inevitably.
Literally, event loop is what JavaScritp uses to implement non-blocking execution. Understanding how the event loops works internally would benefit you a lot when programming in JavaScript.
There are two major environments JavaScript runs in: browser and Node.js.
As mentioned in the book Secrets.of.the.JavaScript.Ninja.2nd.Edition:
Note the difference between handling the macrotask and microtask queues: In a single loop iteration, one macrotask at most is processed (others are left waiting in the queue), whereas all microtasks are processed.
Here comes the first question: why only one marcotask is executed in each iteration?
The HTML standards says the reason.
According to the description, in each iteration, only one task, aka macrotask, is taken from task queue, is executed, while the microtask queue is flushed to empty. After flushing, there's a phase that the browser would evalute the necessity to re-render the UI.
Here is the how the flow works:
Engine starts:
Evaluate mainline JavaScript Code
Registering callbacks, timers, etc
First loop:
take one MacroTask from task queue
flush MicroTasks
optional UI -rendering
Next loop:
take one MacroTask from task queue
flush MicroTasks
optional UI-rendering
Next loop:
take one MacroTask from task queue
flush MicroTasks
optional UI-rendering
Next loop:
...
This loop will continue to the end of the browser page lifetime.
To verify the above loop execution, here are two example snippets;
// you can just run these code in the console (press F12 please).
console.info('MANILINE> START');
setTimeout(() => {
console.info('MACRO> setTimeout.1');
Promise.resolve().then(() => {
console.info('MICRO> setTimeout.1.promise.1');
Promise.resolve().then(() => {
console.info('MICRO> setTimeout.1.promise.inner.1');
Promise.resolve().then(() => console.info('MICRO> setTimeout.1.promise.inner.1.inner'));
});
Promise.resolve().then(() => console.info('MICRO> setTimeout.1.promise.inner.2'));
}).then(() => {
console.info('MICRO> setTimeout.1.promise.1.then');
}).then(() => {
console.info('MICRO> setTimeout.1.promise.1.then.then');
});
}, 0);
setTimeout(() => {
console.info('MACRO> setTimeout.2');
Promise.resolve().then(() => console.info('MICRO> setTimeout.2.promise.1'));
Promise.resolve().then(() => console.info('MICRO> setTimeout.2.promise.2'));
}, 0);
setTimeout(() => console.info('MACRO> setTimeout.3'), 0);
console.info('MAINLINE> END');
If you open a new tab, press F12, copy and run the above code, here comes the output.
MANILINE> START
MAINLINE> END
# this line is empty
MACRO> setTimeout.1
MICRO> setTimeout.1.promise.1
MICRO> setTimeout.1.promise.inner.1
MICRO> setTimeout.1.promise.inner.2
MICRO> setTimeout.1.promise.1.then
MICRO> setTimeout.1.promise.inner.1.inner
MICRO> setTimeout.1.promise.1.then.then
MACRO> setTimeout.2
MICRO> setTimeout.2.promise.1
MICRO> setTimeout.2.promise.2
MACRO> setTimeout.3
As we can clearly see the execution flow.
And another example is actually an html file that you need to load from the browser at the very beginning. The code would run as expected also.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Event loop</title>
</head>
<body>
<div> Division 1 </div>
<script>
console.info('MAINLINE> script.1 START');
setTimeout(() => {
Promise.resolve().then(() => {
console.info('MICRO> script.1.timeout.1.promise');
});
console.info('MACRO> script.1.timeout.1');
});
setTimeout(() => {
console.info('MACRO> script.1.timeout.2');
});
console.info('MAINLINE> script.1 END');
</script>
<div> Divison 2 </div>
<script>
console.info('MAINLINE> script.2 START');
setTimeout(() => {
Promise.resolve().then(() => {
console.info('MICRO> script.2.timeout.1.proimse');
});
console.info('MACRO> script.2.timeout');
});
setTimeout(() => {
console.info('MACRO> script.2.timeout.2');
});
Promise.resolve().then(() => {
console.info('MAINLINE> promise');
});
console.info('MAINLINE> script.2 END');
</script>
<p>The correct logging order in console is </p>
<ol>
<li>MAINLINE> script.1 START</li>
<li>MAINLINE> script.1 END</li>
<li>MAINLINE> script.2 START</li>
<li>MAINLINE> script.2 END</li>
<li>MAINLINE> promise</li>
<li>MACRO> script.1.timeout.1</li>
<li>MICRO> script.1.timeout.1.promise</li>
<li>MACRO> script.1.timeout.2</li>
<li>MACRO> script.2.timeout.1</li>
<li>MICRO> script.2.timeout.1.proimse</li>
<li>MACRO> script.2.timeout.2</li>
</ol>
</body>
</html>
Event loop is a bit different running in Node.js environment.
Though the official document illustrates something on the event loop, I think the tutorial from IBM is better.
Before dive into the execution flow, we have some basic glossaries to clarify.
Microtasks execute immediately following mainline and after every phase of the event loop.
Microtasks are callbacks form process.nextTick()
and then()
handlers for Promises. (process.nextTick()
has higher priority to Promise
.)
Timers have two categories: Immediate and Timeout.
An immediate timer is a Node object that runs immediately during the next Check phase.
A timeout timer is a Node object that runs a callback as soon as possible after the timer expires.
Once the timer expires, the callback is invoked during the next Timers phase of the event loop.
There are two types of Timeout timers: Interval and Timeout.
When there are no more expired timer callbacks to run, the event loop runs any microtasks.
After running microtasks, the event loop moves to the Pending phase.
Certain system-level callback are executed during this phase. You don’t really need to worry about this phase.
Apparently, this phase is “only used internally”. You don’t really need to worry about this phase.
I/O callbacks are executed during this phase.
Normally, if the poll queue is empty, it blocks and waits for any in-flight I/O operations to complete, then execute their callbacks right away.
However, if timers are scheduled the poll phase will end.
Any microtasks will be run as necessary, and the event loop proceeds to the check phase.
This phase is a sort of “post I/O” phase during which only setImmediate()
callbacks are executed. This allows you to run code that executes as soon as the poll phase becomes idle.
Once the check phase callback queue is empty, any microtasks run, and the event loop proceeds to the close phase.
This phase is executed if a socket or handle is closed suddenly.
The following code should be run like node <file>.js
.
const fs = require('fs');
const EventEmitter = require('events').EventEmitter;
const ITERATIONS_MAX = 3;
let iteration = 0;
const event = new EventEmitter();
console.info('MAINLINE> START');
console.info('MAINLINE> Registering Event');
event.on('simple', (eventName, message, source, timestamp) => {
console.info('EVENTS> Received event: name "%s", message "%s", source "%s", timestamp %s', eventName, message, source, timestamp);
});
const hrtime = process.hrtime().join('.');
event.emit('simple', 'simpleEvent', 'Custom message', 'MAINLINE', hrtime);
Promise.resolve().then(() => {
console.info('MAINLINE> promise.1');
});
process.nextTick(() => {
console.info('MAINLINE> process.nextTick');
});
Promise.resolve().then(() => {
console.info('MAINLINE> promise.2');
});
const interval = setInterval(() => {
console.info('TIMERS PHASE> START: iteration %d: setInterval', iteration);
if (iteration >= ITERATIONS_MAX ) {
console.info('TIMERS PHASE> Max interval count exceeded. Goodbye.');
clearInterval(interval);
} else {
{
const htime = process.hrtime().join('.');
event.emit('simple', 'simpleEvent', 'Custom message A from ' + iteration, 'setInterval.EventEmitter', htime);
}
setTimeout((ite) => {
console.info('TIMERS EXPIRED (from iteration %d)> setInterval.setTimeout', ite);
Promise.resolve().then(() => {
console.info('TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 1');
});
process.nextTick(() => {
console.info('TIMERS PHASE MICROTASK> setInterval.setTimeout.process.nextTick');
});
Promise.resolve().then(() => {
console.info('TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 2');
});
}, 0, iteration);
fs.readdir('./', (err, files) => {
if (err) throw err;
console.info('POLL PHASE> fs.readdir() callback');
process.nextTick(() => {
console.info('POLL PHASE MICROTASK> setInterval.fs.readdir.process.nextTick');
});
});
setImmediate(() => {
console.info('CHECK PHASE> setInterval.setImmediate');
process.nextTick(() => {
console.info('CHECK PHASE MICROTASK> setInterval.setImmediate.process.nextTick');
});
});
{
const htime = process.hrtime().join('.');
event.emit('simple', 'simpleEvent', 'Custom message B from ' + iteration, 'setInterval.EventEmitter', htime);
}
process.nextTick((ite) => {
console.info('TIMER PHASE MICROTASK> iteration %d, setInterval.process.nextTick', ite);
}, iteration);
}
console.info('TIMERS PHASE> END: iteration %d: setInterval', iteration);
iteration ++;
}, 0);
console.info('MAINLINE> END');
After running the above scripts, the terminal would print like:
MAINLINE> START
MAINLINE> Registering Event
EVENTS> Received event: name "simpleEvent", message "Custom message", source "MAINLINE", timestamp 385796.462973198
MAINLINE> END
MAINLINE> process.nextTick
MAINLINE> promise.1
MAINLINE> promise.2
TIMERS PHASE> START: iteration 0: setInterval
EVENTS> Received event: name "simpleEvent", message "Custom message A from 0", source "setInterval.EventEmitter", timestamp 385796.464664300
EVENTS> Received event: name "simpleEvent", message "Custom message B from 0", source "setInterval.EventEmitter", timestamp 385796.465196848
TIMERS PHASE> END: iteration 0: setInterval
TIMER PHASE MICROTASK> iteration 0, setInterval.process.nextTick
POLL PHASE> fs.readdir() callback
POLL PHASE MICROTASK> setInterval.fs.readdir.process.nextTick
CHECK PHASE> setInterval.setImmediate
CHECK PHASE MICROTASK> setInterval.setImmediate.process.nextTick
TIMERS EXPIRED (from iteration 0)> setInterval.setTimeout
TIMERS PHASE MICROTASK> setInterval.setTimeout.process.nextTick
TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 1
TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 2
TIMERS PHASE> START: iteration 1: setInterval
EVENTS> Received event: name "simpleEvent", message "Custom message A from 1", source "setInterval.EventEmitter", timestamp 385796.466206990
EVENTS> Received event: name "simpleEvent", message "Custom message B from 1", source "setInterval.EventEmitter", timestamp 385796.466439664
TIMERS PHASE> END: iteration 1: setInterval
TIMER PHASE MICROTASK> iteration 1, setInterval.process.nextTick
POLL PHASE> fs.readdir() callback
POLL PHASE MICROTASK> setInterval.fs.readdir.process.nextTick
CHECK PHASE> setInterval.setImmediate
CHECK PHASE MICROTASK> setInterval.setImmediate.process.nextTick
TIMERS EXPIRED (from iteration 1)> setInterval.setTimeout
TIMERS PHASE MICROTASK> setInterval.setTimeout.process.nextTick
TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 1
TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 2
TIMERS PHASE> START: iteration 2: setInterval
EVENTS> Received event: name "simpleEvent", message "Custom message A from 2", source "setInterval.EventEmitter", timestamp 385796.468226076
EVENTS> Received event: name "simpleEvent", message "Custom message B from 2", source "setInterval.EventEmitter", timestamp 385796.468287021
TIMERS PHASE> END: iteration 2: setInterval
TIMER PHASE MICROTASK> iteration 2, setInterval.process.nextTick
POLL PHASE> fs.readdir() callback
POLL PHASE MICROTASK> setInterval.fs.readdir.process.nextTick
CHECK PHASE> setInterval.setImmediate
CHECK PHASE MICROTASK> setInterval.setImmediate.process.nextTick
TIMERS EXPIRED (from iteration 2)> setInterval.setTimeout
TIMERS PHASE MICROTASK> setInterval.setTimeout.process.nextTick
TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 1
TIMERS PHASE MICROTASK> setInterval.setTimeout.promise 2
TIMERS PHASE> START: iteration 3: setInterval
TIMERS PHASE> Max interval count exceeded. Goodbye.
TIMERS PHASE> END: iteration 3: setInterval
As we can see:
-
First, mainline runs, followed by its microtask, (remeber,
process.nextTick
comes beforepromise
). -
Then, Timers phase and its microtask, (again,
process.nextTick
comes beforepromise
). -
Then, Poll phase and its microtask.
-
Then, Check phase and its microtask.
-
And then, loop to the next Timers phase, and so on.
You can check the illustration image again.
And one thing about the EventEmitter. We register a callback for some event, which would be called in the future, asynchronously. When we make the specific event emitted, it will execute synchronously at that emitting moment.
Don't get confused.
Much efforts has been made to figure out it thoroughly. It's may be not easy to understand event loop in such a short article, you could read more if you do need a deeper understanding in event loop.
Hope this would help you in your JavaScript programming!