On this article we are going to get a bit deeper into the Node environment and understand how it works under the surface. The topics that will cover are:
How does the runtime environment and the JS engine relate to each other?
Nodejs is a runtime environment for running Javascript. What is a runtime environment, you might say? Let us take the example of a browser.
Every browser has a Javascript engine that converts Javascript to machine code so that the computer can understand. So, in other words, this engine is what allows us to create variables, functions and interact with the browser in a more readable way.
A runtime environment is a place on your system where you can use common libraries, environment variables and actually execute your code. In the browser, for instance, you can access the document and window global variables and work with them. In Node there are also common variables that you can use in its runtime environment that we will discuss later on.
Safari, Firefox, Chrome have their own runtime environments, these environments have their own Javascript engines work respectively called Chakra, SpiderMonkey and V8. The latter is the one that we care about right now.
Besides converting Javascript code to machine code a Engine has also some other responsibilities:
- It provides the Garbage Collector → Used for clearing the store of variables and functions that are no longer in use in the execution (out of scope)
- Executes your code and converts Javascript to machine code so that the computer can understand as mentioned before
- Managing memory allocation
- Provide all the data types, operators, objects and functions
- Handling Call Stack (Executing your code in order)
- Event loop
More on what V8 does on this link
The Call Stack and Heap
The Call Stack is the very part of NodeJs that keeps track of your application’s execution. Basically it is a FILO (First in Last Out) that knows exactly where you are in the execution.
The Heap is a memory space that is allocated for your execution. Every time you are in a function scope, the variables that you declare are all stored in the V8 heap. Then, when the execution no longer needs that variable, the garbage collector is in charge of cleaning the heap for the variables of the new function.
Here is an easy example:
function multiply(n, n){ return n * n }function square(n){ const resultMultiply = multiply(n, n) return resultMultiply }function squaredNumber(n){ const result = square(n) return result }
The code above would fill our call stack and heap.
Below you see what happens in action:
Core Modules and C++ Bindings
This image is used everywhere but many people struggle to understand. It is actually pretty simple. Try to look at it horizontally.
- On the top you have your Javacript code.
- On the middle part there is the V8 engine along with core Modules, C+ + Bindings, libuv, http requests, etc.
- And at the bottom, we have the Operating System.
The middle part is what matters for us the most right now.
Core Modules
These modules are made of some objects to enhance Javascript capabilities on the server-side. Some of the most used and some examples are below:
- http → Used for making http requests.
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('This is a test for creating a Nodejs server')
}).listen(3000)
- fs → Used for working with the File System (file creation, deletion, streaming and so on).
const createFile = (filePath, fileContent) => {
fs.writeFile(filePath, fileContent, (error) => {
if(error)
console.error('an error occurred: ', error)
else
console.info('Your file is made')
})
- path → Used for dealing with file paths
path.join(DIRNAME, 'media')
And so on… There are many functions and objects that come out of the box with Nodejs. If you want, you can explore them by downloading Node on your computer and running node
, then clicking tab
twice.
> node
Welcome to Node.js v13.13.0.
Type ".help" for more information.
>
Array ArrayBuffer Atomics
BigInt BigInt64Array BigUint64Array
Boolean Buffer DataView
Date Error EvalError
Float32Array Float64Array Function
GLOBAL Infinity Int16Array
Int32Array Int8Array Intl
JSON Map Math
NaN Number Object
Promise Proxy RangeError
ReferenceError Reflect RegExp
Set SharedArrayBuffer String
Symbol SyntaxError TextDecoder
TextEncoder TypeError URIError
URL URLSearchParams Uint16Array
Uint32Array Uint8Array Uint8ClampedArray
WeakMap WeakSet WebAssembly
_ _error assert
async_hooks buffer child_process
clearImmediate clearInterval clearTimeout
cluster console crypto
decodeURI decodeURIComponent dgram
dns domain encodeURI
encodeURIComponent escape eval
events fs global
globalThis http http2
https inspector isFinite
isNaN module net
os parseFloat parseInt
path perf_hooks process
punycode querystring queueMicrotask
readline repl require
root setImmediate setInterval
setTimeout stream string_decoder
tls trace_events tty
undefined unescape url
util v8 vm
worker_threads zlib
C++ Bindings
C++ Bindings refers to ways that you can use C++ code if you want to use add-ons on you project. Since Nodejs is written in C++, you can easily bind these scripts to work side by side with your Nodejs project.
But why would you use such thing?
- You can do it for performance reasons.
- You may want to integrate a third party library written in C++ to your Nodejs project.
- Or, maybe, access some OS APIs that are difficult to implement using Nodejs alone
Is NodeJs actually single threaded?
Have you ever heard that Nodejs is single-threaded? It is single threaded, but that is not always the case. What does that actually mean?
More on Threads…
To understand more about threads inside of systems we can look at our own computer. Our operating system executes different processes that can be executed by opening a web browser or even a calculator. These processes can contain threads. You can see thread as a line of execution that shares the same memory space inside of a process, or metaphorically…
you think of a highway lane (process). The more lanes (threads) this highway has, more cars (data) can reach their destination (be executed)
“So… if Nodejs is so fast and reliable and it is single threaded, how can it deal with so much data being sent and received at the same time?”
Asynchronous Code In Javascript
The asynchronous nature of Nodejs is what takes the credit for all of that performance boost. To simplify asynchronicity, I think that the best way to explain is:
It’s part of the code that doesn’t block the original execution flow if the response has not yet arrived. It will wait for the response to arrive and when it does, it will send the response after all the code has done with its main execution flow…
For those who had a hard time understanding there is an easier analogy:
Imagine you order a sandwich, the waiter will write down your order and send this information so the chef can start making your order. Usually the waiter will wait on someone else while your sandwich is being made. This is asynchronicity.
Now, if the waiter waited for your order to be ready to wait on someone else, then it would be a synchronous activity.
Usually on Nodejs, it is recommended that you use asynchronous whenever possible, since it greatly increases your application’s performance.
For that, Nodejs provides asynchronous callback functions or promises. Here is an example using Promises:
const getUserInfo = (userId) => new Promise((resolve, reject) => { const user = getUserOnDatabase(userId).then(res => { if(res) resolve(res) else reject() })getUserInfo(userId) .then((res) => { //continues the execution here }) .catch(err => {})...
So what is happening here?
getUserInfo()
function is grabbing some user information. However, since we created a promise to handle this request, it will take some time for the information to be retrieved. We could not block the code from executing so right after getUserInfo()
is executed, the rest of the code (represented by ...
) will be executed and what is wrapped around our then
method will wait for the promise to resolve to execute.
So is NodeJs Single-Threaded or Multi-Threaded?
You though I was forgetting about it, don’t you? But in fact this question is not a Yes or No question but rather, “it depends”.
For some asynchronous calls the libuv library provides a thread-pool with four threads to process the code. This means that the program continues running on a single thread — let us call it the “main thread” — and delegates a separate part with four more threads for the asynchronous calls… So in this case you do have more threads running along side one another.
However, nowadays, more and more OS’s provide asynchronous interfaces for many I/O tasks and libuv does prefer that option than using the thread-pool. In this case, the main thread will prevail.
Some other libuv responsibilities are:
- TCP and UDP sockets of the
net
package - Async DNS resolutions
- Async file and file system operations
- File System events
and so on… more on libuv here
Libuv can also be called a layer that abstracts processing I/O APIs from the upper layers of Nodejs. Along with that, libuv also provides the Event Loop for the whole NodeJS runtime environment.
Event Loop? How does it work ?
Whenever you run your node process (as learned above) The Event Loop is executed. So we have 2 different parts running at the same time on the same thread: V8 and the Event Loop
Here is a little image to explain a bit the architecture behind the event loop and asynchronous events, this cycle is an abstraction of the design pattern reactor pattern:
Given the image above you might be asking: what the $@! is a Event Demultiplexer? What about Low Level I/O Mechanisms?
It is quite complex to explain the details but all you have to know is that it is a layer that has been implemented by numerous of operating systems so that Nodejs can interact with non-blocking asynchronous hardware I/O functionalities. From that you can already tell who is responbile for that right? Yes you are right, libuv!
Basically what happens is:
- you start the Nodejs process by executing something like
node example.js
…Then your application starts running the Event Loop. - Whenever there is a asynchronous callback, promise or some asynchronous API, Node will call the libuv APIs to process these functions.
- These APIs will interact with low-level I/O Mechanisms.
- These mechanisms will send back processed functionalities to libuv (or the event demultiplexer).
- Then, Libuv sends the processed data (data from a file, some request that retrieved a response…etc) to its famous Event Queue — which I will explain in a bit.
- After retrieving data from the queue, it will be sent to the Call Stack to be executed after all synchronous code is executed.
And then, this whole cycle happens again and again until the process ends and the code stops executing.
Event Queue
coming soon…
Conclusion
Javascript is a constant learning. It is a very powerful tool given that it made on top of a very solid and fast language (c++) and can be used for several types of applications thanks to the Event Loop and the asynchronicity nature of the language. However, it is crucial that you understand how it works beneath the surface to take the most out of this powerful tool.
This article has been published from the source link without modifications to the text. Only the headline has been changed.