Node.js: što je to, kada i kako ga koristiti i zašto biste trebali

Vjerojatno ste već pročitali ove rečenice ...

Node.js je JavaScript izvršavanje izgrađeno na Chromeovom V8 JavaScript engineNode.js koristi asinhroni ne-blokirajući I / O modelNode.js koji upravlja događajima na petlji događaja jedne niti

... i ostali su se pitali što sve ovo znači. Nadamo se da ćete do kraja ovog članka bolje razumjeti ove pojmove, kao i ono što je Node, kako djeluje i zašto je dobro koristiti ga.

Krenimo od prolaska kroz terminologiju.

Ulaz / izlaz (ulaz / izlaz)

Kratko od ulaza / izlaza, I / O se prvenstveno odnosi na interakciju programa s diskom i mrežom sustava. Primjeri I / O operacija uključuju čitanje / pisanje podataka s / na disk, izradu HTTP zahtjeva i razgovor s bazama podataka. Vrlo su spori u odnosu na pristup memoriji (RAM-u) ili rad na CPU-u.

Sinkroni vs Asinkroni

Sinkrono (ili sinkronizirano) izvršavanje obično se odnosi na izvršavanje koda u nizu. U sinkroniziranom programiranju, program se izvodi redak po redak, jedan po jedan redak. Svaki put kad se funkcija pozove, izvršavanje programa čeka dok se ta funkcija ne vrati prije nego što nastavi do sljedećeg retka koda.

Asinkrono (ili asinkrono) izvršavanje odnosi se na izvršavanje koje se ne izvodi u slijedu koji se pojavljuje u kodu. U async programiranju program ne čeka završetak zadatka i može prijeći na sljedeći zadatak.

U sljedećem primjeru, operacija sinkronizacije uzrokuje da se upozorenja aktiviraju u nizu. U operaciji asinkronizacije, dok se čini da se upozorenje (2) izvršava drugo, to se ne događa.

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

Akcija sinkronizacije često je povezana s I / O-om, iako setTimeoutje primjer nečega što nije I / O, ali još uvijek async. Općenito govoreći, sve što je povezano s računanjem je sinkronizacija, a sve što je povezano s ulazom / izlazom / vremenom je asinkronizirano. Razlog da se I / O operacije obavljaju sinkrono je taj što su vrlo spori i u suprotnom bi blokirali daljnje izvršavanje koda.

Blokiranje vs Neblokiranje

Blokiranje se odnosi na operacije koje blokiraju daljnje izvršavanje sve dok se ta operacija ne završi, dok se neblokiranje odnosi na kôd koji ne blokira izvršenje. Ili kako kaže Node.js docs, blokiranje je kada izvršavanje dodatnih JavaScript-a u Node.js procesu mora pričekati dok se ne dovrši operacija koja nije JavaScript.

Metode blokiranja izvršavaju se sinkrono, dok se neblokirajuće metode izvršavaju asinkrono.

// 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

U prvom primjeru gore, pozvat console.logće se prije moreWork(). U drugom primjeru fs.readFile()se ne blokira, tako da se izvršavanje JavaScript-a može nastaviti i moreWork()prvo će se pozvati.

U Nodeu se neblokiranje prvenstveno odnosi na I / O operacije, a JavaScript koji pokazuje slabe performanse zbog intenzivnog CPU-a, a ne čekanja na ne-JavaScript operaciji, kao što je I / O, obično se ne naziva blokiranjem.

Sve I / O metode u standardnoj knjižnici Node.js pružaju async verzije koje ne blokiraju i prihvaćaju funkcije povratnog poziva. Neke metode imaju i blokade koji imaju imena koja završavaju sinkronizacijom.

Neblokirajuće U / I operacije omogućuju jednom procesu da istovremeno služi više zahtjeva. Umjesto da se proces blokira i čeka da se I / O operacije dovrše, I / O operacije se delegiraju na sustav, tako da proces može izvršiti sljedeći dio koda. Neblokirajuće U / I operacije pružaju funkciju povratnog poziva koja se poziva kad je operacija dovršena.

Povratni pozivi

Povratni poziv je funkcija prošao kao argument u nekom drugom funkcijom, koja onda može tražiti (nazvao) unutar vanjske funkcije za završetak neke aktivnosti na prikladnije vrijeme. Pozivanje može biti trenutno (sinkroniziranje povratnog poziva) ili se može dogoditi kasnije (asinkroni povratni poziv).

// 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 

U prvom primjeru, funkcija povratnog poziva poziva se odmah unutar funkcije vanjskog pozdrava i prijavljuje se na konzolu prije moreWork()nastavka.

U drugom primjeru, fs.readFile (async metoda koju pruža Node) čita datoteku i kada završi poziva funkciju povratnog poziva s pogreškom ili sadržajem datoteke. U međuvremenu program može nastaviti izvršavanje koda.

Asinhroni povratni poziv može se pozvati kada se dogodi događaj ili kada se zadatak dovrši. Sprječava blokiranje dopuštanjem izvođenja drugog koda u međuvremenu.

Umjesto da proceduralno čitaju kod odozgo prema dolje, asinkroni programi mogu izvršavati različite funkcije u različito vrijeme, ovisno o redoslijedu i brzini kako se događaju ranije funkcije poput http zahtjeva ili čitanja datotečnog sustava. Koriste se kada ne znate kada će se dovršiti neka asinkrna operacija.

Trebali biste izbjegavati " pakao povratnog poziva ", situaciju u kojoj su povratni pozivi ugniježđeni unutar ostalih povratnih poziva duboko nekoliko razina, što otežava razumijevanje, održavanje i otklanjanje pogrešaka u kodu.

Događaji i programiranje vođeno događajima

Događaji su radnje koje generira korisnik ili sustav, poput klika, dovršenog preuzimanja datoteke ili hardverske ili softverske pogreške.

Programiranje vođeno događajima je programska paradigma u kojoj tijek programa određuju događaji. Program vođen događajima izvodi radnje kao odgovor na događaje. Kada se događaj dogodi, aktivira funkciju povratnog poziva.

Pokušajmo sada razumjeti Node i vidjeti kako se sve to odnosi na njega.

Node.js: što je to, zašto je stvoreno i kako to funkcionira?

Jednostavno rečeno, Node.js je platforma koja izvršava JavaScript programe na poslužitelju koji mogu komunicirati s I / O izvorima poput mreža i datotečnih sustava.

Kada je Ryan Dahl 2009. stvorio Node, tvrdio je da se s I / O-om rukuje pogrešno, blokirajući cijeli proces zbog sinkronog programiranja.

Tradicionalne tehnike web-posluživanja koriste model niti, što znači jednu nit za svaki zahtjev. Budući da u I / O operaciji zahtjev provodi većinu vremena čekajući da se dovrši, intenzivni I / O scenariji podrazumijevaju veliku količinu neiskorištenih resursa (poput memorije) povezanih s tim nitima. Stoga se model "jedne niti po zahtjevu" za poslužitelj ne prilagođava dobro.

Dahl je tvrdio da bi softver trebao biti sposoban za višestruke zadatke i predložio uklanjanje vremena provedenog u čekanju da se I / O rezultati vrate. Umjesto modela niti, rekao je da je pravi način za rukovanje nekoliko istodobnih veza imati jednonitnu nit, petlju događaja i neblokirajuće U / I. Na primjer, kada upitate bazu podataka, umjesto da čekate odgovor, dajete joj povratni poziv kako bi se vaše izvršavanje moglo provući kroz tu izjavu i nastaviti raditi druge stvari. Kad se rezultati vrate, možete izvršiti povratni poziv.

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.