Node.js: mi ez, mikor és hogyan kell használni, és miért kellene

Valószínűleg már olvasta ezeket a mondatokat ...

A Node.js egy JavaScript futásidejű alkalmazás, amely a Chrome V8 JavaScript motorjára épül. A Node.js eseményvezérelt, aszinkron nem blokkoló I / O modellt használ. A Node.js egyetlen szál eseményhurokban működik

… És azon tűnődtek, vajon mit jelent mindez. Remélhetőleg a cikk végére jobban megismerheti ezeket a kifejezéseket, valamint azt, hogy mi is a Node, hogyan működik, és miért és mikor érdemes használni.

Kezdjük azzal, hogy áttekintjük a terminológiát.

I / O (bemenet / kimenet)

A bemenet / kimenet rövidítése az I / O elsősorban a program interakciójára utal a rendszer lemezével és hálózatával. Az I / O műveletek példái közé tartozik az adatok olvasása / írása lemezről / lemezre, HTTP kérések készítése és beszélgetés az adatbázisokkal. Nagyon lassúak ahhoz, hogy hozzáférjenek a memóriához (RAM), vagy a CPU-n végezzenek munkát.

Szinkron vs aszinkron

A szinkron (vagy szinkron) végrehajtás általában a kód egymás utáni végrehajtására utal. A szinkron programozás során a program soronként, egyenként soronként kerül végrehajtásra. A függvény minden egyes meghívásakor a program végrehajtása megvárja, amíg a függvény visszatér, mielőtt folytatná a következő kódsort.

Az aszinkron (vagy aszinkron) végrehajtás olyan végrehajtásra utal, amely nem a kódban megjelenő sorrendben fut. Az aszinkron programozásban a program nem várja meg a feladat befejezését, és továbbléphet a következő feladatra.

A következő példában a szinkronizálási művelet a riasztásokat egymás után aktiválja. Az aszinkron művelet során, miközben a (2) riasztás másodiknak tűnik, addig nem.

// Synchronous: 1,2,3 alert(1); alert(2); alert(3); // Asynchronous: 1,3,2 alert(1); setTimeout(() => alert(2), 0); alert(3);

Az aszinkron műveletek gyakran I / O-hoz kapcsolódnak, bár setTimeoutpélda valamire, ami nem I / O, de mégis aszinkron. Általánosságban elmondható, hogy bármi, ami a számításokkal kapcsolatos, szinkronizálás, és bármi, ami a bemenettel / kimenettel / időzítéssel kapcsolatos, aszinkron. Az I / O műveletek aszinkron végrehajtása az oka, hogy nagyon lassúak, és különben blokkolják a kód további végrehajtását.

Blokkolás vs blokkolás nélküli

A blokkolás olyan műveletekre vonatkozik, amelyek blokkolják a további végrehajtást, amíg a művelet be nem fejeződik, míg a nem blokkolás olyan kódra vonatkozik, amely nem blokkolja a végrehajtást. Vagy ahogy a Node.js docs fogalmaz, a blokkolás akkor következik be, amikor a Node.js folyamatban a további JavaScript végrehajtásának meg kell várnia, amíg egy nem JavaScript művelet befejeződik.

A blokkolási módszerek szinkron, míg a nem blokkoló módszerek aszinkron módon hajtanak végre.

// Blocking const fs = require('fs'); const data = fs.readFileSync('/file.md'); // blocks here until file is read console.log(data); moreWork(); // will run after console.log // Non-blocking const fs = require('fs'); fs.readFile('/file.md', (err, data) => { if (err) throw err; console.log(data); }); moreWork(); // will run before console.log

A fenti első példában korábban console.loghívjuk meg moreWork(). A második példa fs.readFile()nem blokkoló, így a JavaScript futtatása folytatódhat, és moreWork()először hívják meg.

A Node-ban a nem blokkolás elsősorban az I / O műveletekre utal, és a JavaScriptet, amely gyenge teljesítményt mutat, mivel a CPU intenzív, és nem egy nem JavaScript műveletre, például az I / O-ra vár, nem szokták blokkolásnak nevezni.

A Node.js szabványos könyvtár összes I / O metódusa aszinkron verziókat biztosít, amelyek nem blokkolják és elfogadják a visszahívási funkciókat. Egyes módszereknek blokkoló társaik is vannak, amelyeknek neve szinkronizálással végződik.

A nem blokkoló I / O műveletek lehetővé teszik, hogy egyetlen folyamat egyszerre több kérést is kiszolgáljon. A folyamat blokkolása és az I / O műveletek befejezésének megvárása helyett az I / O műveleteket a rendszerre ruházzák, hogy a folyamat végrehajthassa a következő kódrészletet. A nem blokkoló I / O műveletek visszahívási funkciót biztosítanak, amelyet a művelet befejeztével hívnak meg.

Visszahívások

A visszahívás egy olyan függvény, amelyet argumentumként átadnak egy másik függvénynek, amelyet aztán a külső függvény belsejében lehívhatunk (visszahívhatunk), hogy valamilyen műveletet végrehajtsunk kényelmes időben. A meghívás lehet azonnali (visszahívás szinkronizálása), vagy később is bekövetkezhet (aszinkron visszahívás).

// Sync callback function greetings(callback) { callback(); } greetings(() => { console.log('Hi'); }); moreWork(); // will run after console.log // Async callback const fs = require('fs'); fs.readFile('/file.md', function callback(err, data) { // fs.readFile is an async method provided by Node if (err) throw err; console.log(data); }); moreWork(); // will run before console.log 

Az első példában a visszahívási funkció azonnal meghívásra kerül a külső üdvözlő funkción belül, és a moreWork()folytatás előtt bejelentkezik a konzolra .

A második példában az fs.readFile (a Node által biztosított aszinkron módszer) beolvassa a fájlt, és amikor befejezi, hibával vagy a fájl tartalmával hívja meg a visszahívási funkciót. Addig a program folytathatja a kódfuttatást.

Aszinkron visszahívást akkor lehet hívni, amikor esemény történik, vagy amikor egy feladat befejeződik. Megakadályozza a blokkolást azáltal, hogy közben más kódot is futtat.

Az aszinkron programok eljárási eljárástól fentről lefelé olvasása helyett különböző funkciókat hajthatnak végre különböző időpontokban, a korábbi funkciók, például a http-kérések vagy a fájlrendszer-olvasások sorrendje és sebessége alapján. Akkor használják, ha nem tudja, mikor fejeződik be valamilyen aszinkron művelet.

Kerülnie kell a „ visszahívási poklot ”, egy olyan helyzetet, amikor a visszahívások több szint mélységben be vannak ágyazva más visszahívásokba, ami megnehezíti a kód megértését, karbantartását és hibakeresését.

Események és eseményvezérelt programozás

Az események a felhasználó vagy a rendszer által létrehozott műveletek, például kattintás, befejezett fájlletöltés vagy hardver- vagy szoftverhiba.

Az eseményvezérelt programozás olyan programozási paradigma, amelyben a program folyamatát az események határozzák meg. Egy eseményvezérelt program az eseményekre reagálva végez műveleteket. Amikor egy esemény bekövetkezik, visszahívási funkciót indít el.

Most próbáljuk megérteni a Node-ot, és nézzük meg, hogy ezek mind kapcsolatban állnak vele.

Node.js: mi ez, miért hozták létre és hogyan működik?

Egyszerűen fogalmazva: a Node.js egy olyan szerveroldali JavaScript programot futtató platform, amely képes kommunikálni az I / O forrásokkal, például hálózatokkal és fájlrendszerekkel.

Amikor Ryan Dahl 2009-ben létrehozta a Node-ot, azzal érvelt, hogy az I / O-t helytelenül kezelik, és a szinkron programozás miatt blokkolja az egész folyamatot.

A hagyományos web-kiszolgáló technikák a szálmodellt használják, minden kéréshez egy szálat. Mivel egy I / O művelet során a kérelem az idő nagy részét arra vár, hogy befejezze, az intenzív I / O forgatókönyvek nagy mennyiségű, fel nem használt erőforrást (például memóriát) tartalmaznak, amelyek ezekhez a szálakhoz kapcsolódnak. Ezért a szerver „kérésenként egy szál” modellje nem méretezhető jól.

Dahl argued that software should be able to multi-task and proposed eliminating the time spent waiting for I/O results to come back. Instead of the thread model, he said the right way to handle several concurrent connections was to have a single-thread, an event loop and non-blocking I/Os. For example, when you make a query to a database, instead of waiting for the response you give it a callback so your execution can run through that statement and continue doing other things. When the results come back you can execute the callback.

The event loop is what allows Node.js to perform non-blocking I/O operations despite the fact that JavaScript is single-threaded. The loop, which runs on the same thread as the JavaScript code, grabs a task from the code and executes it. If the task is async or an I/O operation the loop offloads it to the system kernel, like in the case for new connections to the server, or to a thread pool, like file system related operations. The loop then grabs the next task and executes it.

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes (this is an event), the kernel tells Node.js so that the appropriate callback (the one that depended on the operation completing) may be added to the poll queue to eventually be executed.

Node keeps track of unfinished async operations, and the event loop keeps looping to check if they are finished until all of them are.

To accommodate the single-threaded event loop, Node.js uses the libuv library, which, in turn, uses a fixed-sized thread pool that handles the execution of some of the non-blocking asynchronous I/O operations in parallel. The main thread call functions post tasks to the shared task queue, which threads in the thread pool pull and execute.

Inherently non-blocking system functions such as networking translate to kernel-side non-blocking sockets, while inherently blocking system functions such as file I/O run in a blocking way on their own threads. When a thread in the thread pool completes a task, it informs the main thread of this, which in turn, wakes up and executes the registered callback.

The above image is taken from Philip Roberts’ presentation at JSConf EU: What the heck is the event loop anyway? I recommend watching the full video to get a high level idea about how the event loop works.

The diagram explains how the event loop works with the browser but it looks basically identical for Node. Instead of web APIs we would have Node APIs.

According to the presentation, the call stack (aka execution stack or “the stack”) is a data structure which records where in the program we are. If we step into a function, we put something onto the stack. If we return from a function, we pop it off the top of the stack.

This is how the code in the diagram is processed when we run it:

  1. Push main() onto the stack (the file itself)
  2. Push console.log(‘Hi’); onto the stack, which executes immediately logging “Hi” to the console and gets popped off the stack
  3. Push setTimeout(cb, 5000) onto the stack. setTimeout is an API provided by the browser (on the backend it would be a Node API). When setTimeout is called with the callback function and delay arguments, the browser kicks off a timer with the delay time
  4. The setTimeout call is completed and gets popped off the stack
  5. Push console.log(‘JSConfEU’); onto the stack, which executes immediately logging “JSConfEU” to the console and gets popped off the stack
  6. main() gets popped off the stack
  7. After 5000 milliseconds the API timer completes and the callback gets moved to the task queue
  8. The event loop checks if the stack is empty because JavaScript, being single-threaded, can only do one thing at a time (setTimeout is not a guaranteed but a minimum time to execution). If the stack is empty it takes the first thing on the queue and pushes it onto the stack. Therefore the loop pushes the callback onto the stack
  9. The callback gets executed, logs “there” to the console and gets popped off the stack. And we are done

If you want to go even deeper into the details on how Node.js, libuv, the event loop and the thread pool work, I suggest checking the resources on the reference section at the end, in particular this, this and this along with the Node docs.

Node.js: why and where to use it?

Since almost no function in Node directly performs I/O, the process never blocks (I/O operations are offloaded and executed asynchronously in the system), making it a good choice to develop highly scalable systems.

Due to its event-driven, single-threaded event loop and asynchronous non-blocking I/O model, Node.js performs best on intense I/O applications requiring speed and scalability with lots of concurrent connections, like video & audio streaming, real-time apps, live chats, gaming apps, collaboration tools, or stock exchange software.

Node.js may not be the right choice for CPU intensive operations. Instead the traditional thread model may perform better.

npm

npm is the default package manager for Node.js and it gets installed into the system when Node.js is installed. It can manage packages that are local dependencies of a particular project, as well as globally-installed JavaScript tools.

www.npmjs.com hosts thousands of free libraries to download and use in your program to make development faster and more efficient. However, since anybody can create libraries and there’s no vetting process for submission, you have to be careful about low quality, insecure, or malicious ones. npm relies on user reports to take down packages if they violate policies, and to help you decide, it includes statistics like number of downloads and number of depending packages.

How to run code in Node.js

Start by installing Node on your computer if you don’t have it already. The easiest way is to visit nodejs.org and click to download it. Unless you want or need to have access to the latest features, download the LTS (Long Term Support) version for you operating system.

You run a Node application from your computer’s terminal. For example make a file “app.js” and add console.log(‘Hi’); to it. On your terminal change the directory to the folder where this file belongs to and run node app.js. It will log “Hi” to the console. ?

References

Here are some of the interesting resources I reviewed during the writing of the article.

Node.js presentations by its author:

  • Original Node.js presentation by Ryan Dahl at JSConf 2009
  • 10 Things I Regret About Node.js by Ryan Dahl at JSConf EU 2018

Node, the event loop and the libuv library presentations:

  • What the heck is the event loop anyway? by Philip Roberts at JSConf EU
  • Node.js Explained by Jeff Kunkle
  • In The Loop by Jake Archibald at JSConf Asia 2018
  • Everything You Need to Know About Node.js Event Loop by Bert Belder
  • A deep dive into libuv by Saul Ibarra Coretge at NodeConf EU 2016

Node documents:

  • About Node.js
  • The Node.js Event Loop, Timers, and process.nextTick()
  • Overview of Blocking vs Non-Blocking

Additional resources:

  • Art of Node by Max Ogden
  • Callback hell by Max Ogden
  • What is non-blocking or asynchronous I/O in Node.js? on Stack Overflow
  • Event driven programming on Wikipedia
  • Node.js on Wikipedia
  • Thread on Wikipedia
  • libuv

Thanks for reading.