I've encountered this sentence countless times during interviews or classes, to the point that even my pet goldfish (or imaginary goldfish, if you prefer) could recite it perfectly. However, instead of mindlessly repeating it like a mantra, let's take a closer look at the terms - single-threaded, non-blocking, and event-driven - and delve into the magic that makes NodeJS so fascinating.
Chrome’s V8 Engine
Use of Libuv library
When we talk about NodeJS architecture, it’s impossible to overlook the vital role played by the libuv library. It might not be as famous as V8, but its significance cannot be overstated. Libuv is a multi-platform support library with a primary job to handle asynchronous I/O operations, but it does much more than that. It bridges the gap between different operating systems. It ensures that reading and writing files, for example, works consistently whether the code is running on Windows, macOS or Linux. The library offers a comprehensive set of features beyond basic I/O polling mechanisms. It introduces 'handles' and 'streams' to simplify interaction with sockets and various entities while also providing cross-platform capabilities for file I/O and threading. These are just a few examples of the library's versatile functionality. In essence, libuv is the reason why our Node.js web server can handle thousands of requests simultaneously, and we, as developers, can build efficient, cross-platform applications without tearing our hair out or imagining pet goldfish over platform-specific quirks.
Single Threaded Event Loop
An event loop is an infinite single-threaded loop that keeps running as long as our NodeJS application runs, managing asynchronous operations and ensuring that our program remains responsive by efficiently handling multiple tasks without blocking the execution of other code.
Event loop is really just a loop for callbacks.
Pay special attention to the term ‘a loop’ here as it represents a single-threaded mechanism that tirelessly operates as long as our NodeJS application runs. Its primary job? Managing asynchronous operations without blocking the rest of the code. For instance, if there’s a potentially time-consuming task, like making a DNS query or reading a large file, the event loop doesn’t bring anything to a halt. Instead, it sets a callback, sends the task off to be processed and moves on to other tasks, ensuring our application remains responsive and efficient.
NodeJS, instead of requiring a multi-threaded environment where each connection might require its own thread, takes a relatively efficient approach. When a new connection is established, or a time-consuming task is encountered, instead of creating a new thread, it utilizes an event loop to register callbacks and delegates the work asynchronously. This means that the application can handle new requests and tasks without having to allocate resources to individual threads.
Within Node.js, the thread pool is a lifesaver when it comes to handling resource-intensive tasks that could potentially overwhelm the main thread.
Imagine scenarios like making an HTTP request or performing a DNS query within the event loop. If carried out directly on the main thread, these tasks could bring out application to an abrupt halt. To prevent this, Node.js leverages the power of libuv to spin up new threads within the thread pool.
When the event loop encounters one of these heavy-duty tasks, Node.js intelligently offloads it to the thread pool, freeing up the main thread to continue with other essential tasks. We, as developers, do not have to worry about what goes to the thread pool and what doesn’t.
One of the key features of NodeJS is its ability to handle multiple tasks independently without being blocked, even if the task has not been executed completely. It does not wait for tasks to be complete allowing it to concurrently handle multiple tasks, enabling efficient utilization of resources to improve application performance. Asynchronous programming allows multiple tasks to be executed without blocking the execution of other tasks. Callbacks, promises and async/await are generally used in NodeJS for achieving concurrency.
Node.js is fundamentally built around an event-driven architecture. In an event-driven architecture, programs don’t follow a traditional flow of execution; instead, they respond to external events. These can be anything from a user clicking a button to data arriving from a sensor or event system-level events like a file being created.
Node.js takes a proactive approach to events, akin to Batman vigilantly awaiting the Bat Signal. Instead of waiting for things to happen, it listens to and reacts to events in real time. When an event occurs, it triggers a specific callback function associated with that event, allowing our program to respond dynamically. For instance, imagine we are creating a real-time chat application. When a user sends a message, NodeJS swiftly detects this event and triggers a designated callback function. This enables our program to dynamically respond, ensuring that the message is processed and delivered promptly, exemplifying the power of NodeJS event-driven architecture.
To wrap up, we’ve taken a deep dive into NodeJS’ internal architecture, understanding how the Chrome V8 engine, libuv library, event loop and thread pool collaborate to craft a single-threaded, non-blocking, event-driven environment for creating awesome projects.
Thank you for reading this article. Please consider replying below and subscribing to the blog if you liked this article.