Five Common Misunderstandings for NodeJS Developers

  • 2021-08-31 06:56:04
  • OfStack

Nodejs was born in 2009, and because it uses JavaScript, it has gained wide popularity in these years. It is an JavaScript runtime for writing server-side applications, but the phrase "it is JavaScript" is not 100% true.

JavaScript is single-threaded and is not designed to run on the server side where scalability is required. With Google Chrome's high-performance V8 JavaScript engine, libuv's super-cool asynchronous I/O implementation, and a few other exciting additions, Nodejs is able to bring client-side JavaScript to the server side, allowing you to write ultra-fast Web JavaScript servers that can handle thousands of socket connections.

NodeJS is a large platform built with a large number of interesting basic modules. However, due to a lack of understanding of how these internal components of NodeJS work, many NodeJS developers misunderstand the behavior of NodeJS and develop applications that cause serious performance problems and errors that are difficult to track. In this article, I will describe five common misunderstandings among many NodeJS developers.

Myth 1-EventEmitter is related to event loops

NodeJS EventEmitter is used extensively when writing NodeJS applications, but people mistakenly think that EventEmitter is related to NodeJS Event Loop, which is incorrect.

The NodeJS event loop is the core of NodeJS, which provides an asynchronous, non-blocking I/O mechanism for NodeJS. It handles completion events from different types of asynchronous events in a specific order.

Instead, NodeJS Event Emitter is a core NodeJS API that allows you to attach the listener function to a specific event that is called as soon as it is triggered. This behavior looks asynchronous because the event handler is usually invoked later than it was originally registered as an event handler.

The EventEmitter instance tracks all events associated with events within the EventEmitter instance itself and its instance itself. It does not schedule any events in the event loop queue. The data structure that stores this information is just a plain old-fashioned JavaScript object, where the object property is the event name and the value of the property is a listener function or an array of listener functions.

When the emit function is called on an EventEmitter instance, emitter synchronizes all callback functions registered on the instance in sequence.

Look at the following code snippet:

const EventEmitter = require('events');

const myEmitter = new EventEmitter();

myEmitter.on('myevent', () = > console.log('handler1: myevent was fired!'));
myEmitter.on('myevent', () = > console.log('handler2: myevent was fired!'));
myEmitter.on('myevent', () = > console.log('handler3: myevent was fired!'));

myEmitter.emit('myevent');
console.log('I am the last log line');

The output of the above code snippet is:

handler1: myevent was fired!
handler2: myevent was fired!
handler3: myevent was fired!
I am the last log line

Because event emitter executes all event handlers synchronously, I am the last log line will not print until all listener functions are called.

Myth 2-All functions that accept callbacks are asynchronous

Whether a function is synchronous or asynchronous depends on whether the function creates asynchronous resources during execution. According to this definition, if you are given a function, you can be sure that the given function is asynchronous:

JavaScript

NodeJS
setTimeout, setInterval, setImmediate, process. nextTick

NodeJS API

child_process, fs, net
PromiseAPI
async-await

Call a function from the C + + plug-in that is written as an asynchronous function (for example, bcrypt)

Accepting a callback function as an argument does not make the function asynchronous. However, usually asynchronous functions do accept a callback as the last argument (unless the wrapper returns an Promise). This pattern of accepting a callback and passing the result to the callback is called Continuation Passing Style. You can still write synchronization using Continuation Passing Style.


const sum = (a, b, callback) => {
 callback(a + b);
};

sum(1,2, (result) => {
 console.log(result);
});

Synchronous and asynchronous functions differ greatly in how the stack is used during execution. The synchronization function occupies the stack throughout its execution by prohibiting anyone else from occupying the stack until return. Instead, asynchronous functions schedule a few asynchronous tasks and return immediately, thus removing themselves from the stack. 1 Once the scheduled asynchronous task completes, any callback provided will be called and the callback function will occupy the stack again. At this point, the function that started the asynchronous task will no longer be available because it has already returned.

Considering the above definition, try to determine whether the following functions are asynchronous or synchronous.


function writeToMyFile(data, callback) {
  if (!data) {
    callback(new Error('No data provided'));
  } else {
    fs.writeFile('myfile.txt', data, callback);
  }
}

In fact, the above functions can be synchronous or asynchronous, depending on the value data passed in.

If data is false, callback is called immediately with an error. In this execution path, the functionality is 100% synchronous because it does not perform any asynchronous tasks.

If data is true, it writes data to myfile. txt, and the callback is called after the file I/O operation is completed. This execution path is 100% asynchronous due to the asynchronous file I/O operation.

It is strongly recommended not to write functions in such an unpredictable manner (performing both synchronous and asynchronous operations on this feature), as this can make the behavior of the application unpredictable. Fortunately, these can be easily fixed as follows:


function writeToMyFile(data, callback) {
  if (!data) {
    process.nextTick(() => callback(new Error('No data provided')));
  } else {
    fs.writeFile('myfile.txt', data, callback);
  }
}

process. nextTick can be used to delay calls to the callback function so that the execution path is asynchronous.

Alternatively, you can use setImmediate instead of process. nextTick, which will more or less produce the same result. However, process. nextTick has a relatively higher priority for callbacks, making them faster than setImmediate.

Myth 3-All features that consume a lot of CPU are preventing event loops

It is well known that CPU intensive operations block the Node. js event loop. Although this statement is true to a certain extent, it is not 100% true because some CPU-intensive functions do not block event loops.

1 Generally speaking, encryption operations and compression operations are highly restricted by CPU. For this reason, some encryption functions and asynchronous versions of zlib functions are written to perform calculations on the libuv thread pool so that they do not block event loops. One of the functions is:

crypto.pbkdf2() crypto.randomFill() crypto.randomBytes() All zlib asynchronous features

However, as of this writing, it is not possible to use pure JavaScript to run CPU-intensive operations on an libuv thread pool. However, you can write your own C + + plug-in to enable you to schedule work on the libuv thread pool. There are third-party libraries (such as bcrypt) that perform CPU-intensive operations and use C + + plug-ins to implement asynchronous API for CPU binding operations.

Myth 4-All asynchronous operations are performed on the thread pool

Modern operating systems have built-in kernel support that enables the use of event notifications (for example, epoll in Linux, kqueue in macOS, IOCP in Windows, etc.) to facilitate native asynchronous network I/O operations in an efficient manner. Therefore, the network I/O is not executed on the libuv thread pool.

However, when it comes to the files I/O, there are many inconsistencies in some cases across operating systems and within the same operating system. This makes it extremely difficult to implement a generic platform-independent API for the file I/O. Therefore, file system operations are performed on the libuv thread pool to expose the asynchronous API of 1.

dns. lookup () The function in the dns module is another API that utilizes the libuv thread pool. The reason is that resolving a domain name to an IP address using the dns. lookup () function is a platform-related operation and is not 100% network I/O.

Myth 5-CPU-intensive applications should not be written using NodeJS

This is not really a misunderstanding, but a well-known fact about NodeJS, which is now obsolete due to the introduction of Worker Threads in Node v 10.5. 0. Although it was introduced as an experimental feature, worker_threads has been stable since Node v12 LTS and is therefore suitable for use in production applications with CPU intensive operations.

Each Node. js worker thread will have its own copy of the v8 runtime, event loop, and libuv thread pool. Therefore, one worker thread performing an EN-intensive operation that blocks CPU does not affect the event loops of the other worker threads, making them available for any incoming work.

However, at the time of writing, IDE does not have the greatest support for Worker Threads. Some IDE do not support attaching the debugger to code running in a thread other than the main thread. However, with many developers already using worker threads for CPU binding operations (such as video encoding, etc.), development support will mature over time.


Related articles: