NodeJS #4
Async
$ whoami
Inna Ivashchuk
Senior Software Engineer
JS developer, music fan, movie-dependent and Star Wars fan 🤓
May the Force be with you!
Agenda
-
EventLoop
-
macrotasks vs microtasks
-
setImmediate(fn) vs setTimeout(fn, 0)
-
process.nextTick()
-
Streams
EventLoop
Node.js architecture
JavaScript is “single-threaded” because JavaScript works on “Event Loop”
NodeJS is NOT “single-threaded” as it has a libuv threadpool in its runtime.
The chefs are the libuv threadpool and OS Async Helpers.
The waiter is the V8 engine and the Event Loop
The JavaScript code you want to be executed is the food
Node.js restorant
What is the Event Loop?
The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.
Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed. We'll explain this in further detail later in this topic.
Event Loop Explained
The following diagram shows a simplified overview of the event loop's order of operations.
setTimeout(cb, 0)
when the cb will be executed?
Event-loop phases
Phases Overview
Each phase has a FIFO queue of callbacks to execute and here is the list of them:
- timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
- pending callbacks: executes I/O callbacks deferred to the next loop iteration.
- idle, prepare: only used internally.
- poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); the node will block here when appropriate.
- check: setImmediate() callbacks are invoked here.
- close callbacks: some close callbacks, e.g. socket.on('close', ...).
Phases in details
timers
A timer specifies the threshold after which a provided callback may be executed rather than the exact time a person wants it to be executed. Timers callbacks will run as early as they can be scheduled after the specified amount of time has passed.
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
pending callbacks
This phase executes callbacks for some system operations such as types of TCP errors.
For example, if a TCP socket receives ECONNREFUSED when attempting to connect, some *nix systems want to wait to report the error. This will be queued to execute in the pending callbacks phase.
poll
The poll phase has two main functions:
- Calculating how long it should block and poll for I/O, then
- Processing events in the poll queue.
When the event loop enters the poll phase and there are no timers scheduled, one of two things will happen:
-
If the poll queue is not empty, the event loop will iterate through its queue of callbacks executing them synchronously until either the queue has been exhausted, or the system-dependent hard limit is reached.
-
If the poll queue is empty, one of two more things will happen:
-
If scripts have been scheduled by setImmediate(), the event loop will end the poll phase and continue to the check phase to execute those scheduled scripts.
-
If scripts have not been scheduled by setImmediate(), the event loop will wait for callbacks to be added to the queue, then execute them immediately.
-
check
This phase allows a person to execute callbacks immediately after the poll phase has been completed. If the poll phase becomes idle and scripts have been queued with setImmediate(), the event loop may continue to the check phase rather than waiting.
setImmediate() is actually a special timer that runs in a separate phase of the event loop. It uses a libuv API that schedules callbacks to execute after the poll phase has completed.
close callbacks
If a socket or handle is closed abruptly (e.g. socket.destroy()), the 'close' event will be emitted in this phase. Otherwise it will be emitted via process.nextTick().
macrotasks vs microtasks
macroTasks
setTimeout
setInterval
setImmediate
requestAnimationFrame
microTasks
I/O
UI rendering
process.nextTick
Promises
queueMicrotask
MutationObserver
microTasks and macroTasks
If microtasks continuously add more elements to microTasks queue, macroTasks will stall and won’t complete the event loop in a shorter time causing event loop delays.
function logA() { console.log('A') };
function logB() { console.log('B') };
function logC() { console.log('C') };
function logD() { console.log('D') };
function logE() { console.log('E') };
function logF() { console.log('F') };
function logG() { console.log('G') };
function logH() { console.log('H') };
function logI() { console.log('I') };
function logJ() { console.log('J') };
logA();
setTimeout(logG, 0);
Promise.resolve()
.then(logC)
.then(setTimeout(logH))
.then(logD)
.then(logE)
.then(logF);
setTimeout(logI);
setTimeout(logJ);
logB();
When to use and what?
Basically, use microtasks when you need to do stuff asynchronously in a synchronous way (i.e. when you would say perform this (micro-)task in the most immediate future). Otherwise, stick to macrotasks.
- Microtasks are processed when the current task ends and the microtask queue is cleared before the next macrotask cycle.
- Microtasks can enqueue other microtasks. All are executed before the next task in line.
- UI rendering is run after all microtasks execution (NA for nodejs).
setImmediate(fn) vs setTimeout(fn, 0)
setImmediate() and setTimeout() are similar, but behave in different ways depending on when they are called.
- setImmediate() is designed to execute a script once the current poll phase completes.
- setTimeout() schedules a script to be run after a minimum threshold in ms has elapsed.
setImmediate() vs setTimeout()
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
The main advantage - setImmediate() will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
However, if you move the two calls within an I/O cycle, the immediate callback is always executed first:
setImmediate() vs setTimeout()
process.nextTick()
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
process.nextTick(() => {
//do something
});
});
Every time the event loop takes a full trip, we call it a tick.
When we pass a function to process.nextTick(), we instruct the engine to invoke this function at the end of the current operation, before the next event loop tick starts:
Understanding process.nextTick()
process.nextTick() vs setImmediate()
We have two calls that are similar as far as users are concerned, but their names are confusing.
- process.nextTick() fires immediately on the same phase
- setImmediate() fires on the following iteration or 'tick' of the event loop
In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate(), but this is an artifact of the past which is unlikely to change.
Why use process.nextTick()?
-
Allow users to handle errors, clean up any then unneeded resources, or perhaps try the request again before the event loop continues.
-
At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.
One example is to match the user's expectations. Simple example:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
Streams
What are streams
Streams are one of the fundamental concepts that power Node.js applications.
They are a way to handle reading/writing files, network communications, or any kind of end-to-end information exchange in an efficient way.
The Node.js stream module provides the foundation upon which all streaming APIs are built. All streams are instances of EventEmitter
What are streams
Using Stream API from "fs" module
Why streams
Streams provide two major advantages over using other data handling methods:
- Memory efficiency: you don't need to load large amounts of data in memory before you are able to process it
- Time efficiency: it takes way less time to start processing data, since you can start processing as soon as you have it, rather than waiting till the whole data payload is available
An example of a stream
const http = require('http')
const fs = require('fs')
const server = http.createServer(function(req, res) {
fs.readFile(__dirname + '/data.txt', (err, data) => {
res.end(data)
})
});
server.listen(3000)
Using the Node.js fs module:
An example of a stream
If the file is big, the operation will take quite a bit of time. Here is the same thing written using streams:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
const stream = fs.createReadStream(__dirname + '/data.txt')
stream.pipe(res)
});
server.listen(3000);
pipe()
The return value of the pipe() method is the destination stream, which is a very convenient thing that lets us chain multiple pipe() calls, like this:
src.pipe(dest1).pipe(dest2)
src.pipe(dest1)
dest1.pipe(dest2)
and it's equal to:
pipe()
Streams-powered Node.js APIs
- process.stdin returns a stream connected to stdin
- process.stdout returns a stream connected to stdout
- process.stderr returns a stream connected to stderr
- fs.createReadStream() creates a readable stream to a file
- fs.createWriteStream() creates a writable stream to a file
- net.connect() initiates a stream-based connection
- http.request() returns an instance of the http.ClientRequest class, which is a writable stream
- zlib.createGzip() compress data using gzip (a compression algorithm) into a stream
- zlib.createGunzip() decompress a gzip stream.
- zlib.createDeflate() compress data using deflate (a compression algorithm) into a stream
- zlib.createInflate() decompress a deflate stream
Different types of streams
Writable
Readable
Duplex
Transform
streams to which data can be written (for example, fs.createWriteStream()).
streams from which data can be read (for example, fs.createReadStream())
streams that are both Readable and Writable (for example, net.Socket)
Duplex streams that can modify or transform the data as it is written and read (for example, zlib.createDeflate()).
How to create a readable stream
const Stream = require('stream')
const readableStream = new Stream.Readable()
// can be done like that or as an option
// readableStream._read = () => {}
const readableStream = new Stream.Readable({
read() {}
})
// the stream is initialized, we can send data to it
readableStream.push('hi!')
readableStream.push('ho!')
How to get data from a readable stream
const Stream = require('stream')
const readableStream = new Stream.Readable({
read() {}
})
const writableStream = new Stream.Writable()
writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString())
next()
}
readableStream.pipe(writableStream)
readableStream.push('hi!')
readableStream.push('ho!')
You can also consume a readable stream directly, using the readable event:
readableStream.on('readable', () => {
console.log(readableStream.read())
})
How to create a writable stream
const Stream = require('stream')
const writableStream = new Stream.Writable()
// can be done like that or as an option
// const readableStream = new Stream.Write({
// write() {}
// })
writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString())
next()
}
// the stream is initialized, we can send data to it
process.stdin.pipe(writableStream)
How to send data to a writable stream
writableStream.write('hey!\n')
Using the stream write() method:
const Stream = require('stream')
const readableStream = new Stream.Readable({
read() {}
})
const writableStream = new Stream.Writable()
writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString())
next()
}
readableStream.pipe(writableStream)
readableStream.push('hi!')
readableStream.push('ho!')
writableStream.end()
Signaling a writable stream that you ended writing
How to create a transform stream
We get the Transform stream from the stream module, and we initialize it and implement the transform._transform() method.
First create a transform stream object:
const { Transform } = require('stream')
const TransformStream = new Transform();
// then implement _transform:
TransformStream._transform = (chunk, encoding, callback) => {
console.log(chunk.toString().toUpperCase());
callback();
}
// Pipe readable stream:
process.stdin.pipe(TransformStream);
Q & A
Homework
-
Create a new directory tutorial4
-
Create a functions microTasks and macroTasks and use the corresponding JS tasks inside them like setTimeout(), process.nextTick() an etc.
-
Use a Promise to calculate the Fibonacci sequence
-
Use fs.createReadStream to read a file
-
Use fs.createWriteStream to write a file
-
Use pipe() to read and compress files
Helpful souces
- EventLoop in the Browser
- My repository with NodeJS tutorials
NodeJS Core #4
By Inna Ivashchuk
NodeJS Core #4
- 455