Node.js: какво е това, кога и как да го използвате и защо трябва

Вероятно сте чели тези изречения и преди ...

Node.js е време за изпълнение на JavaScript, изградено на V8 JavaScript engine на Chrome Node.js използва управляван от събития асинхронен неблокиращ I / O modelNode.js работи в цикъл на събитие с една нишка

... и останаха да се чудят какво означава всичко това. Надяваме се, че до края на тази статия ще имате по-добро разбиране за тези термини, както и за това какво е Node, как работи и защо и кога е добра идея да го използвате.

Нека започнем с разглеждане на терминологията.

I / O (вход / изход)

Съкратено от вход / изход, I / O се отнася главно до взаимодействието на програмата с диска и мрежата на системата. Примерите за I / O операции включват четене / запис на данни от / на диск, отправяне на HTTP заявки и разговори с бази данни. Те са много бавни в сравнение с достъпа до памет (RAM) или извършването на работа на процесора.

Синхронен срещу Асинхронен

Синхронното (или синхронизираното) изпълнение обикновено се отнася до код, изпълняван последователно. При синхронизираното програмиране програмата се изпълнява ред по ред, един по ред. Всеки път, когато се извика функция, изпълнението на програмата изчаква, докато тази функция се върне, преди да продължи към следващия ред код.

Асинхронното (или асинхронното) изпълнение се отнася до изпълнение, което не се изпълнява в последователността, която се появява в кода. При асинхронното програмиране програмата не чака изпълнението на задачата и може да премине към следващата задача.

В следващия пример операцията за синхронизиране кара предупрежденията да се активират последователно. В асинхронната операция, докато предупреждението (2) изглежда се изпълнява второ, това не става.

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

Асинхронната операция често е свързана с I / O, въпреки че setTimeoutе пример за нещо, което не е I / O, но все още е async. Най-общо казано, всичко, свързано с изчисленията, е синхронизиране и всичко, свързано с входа / изхода / времето, е асинхронно. Причината за I / O операциите да се извършват асинхронно е, че те са много бавни и в противен случай биха блокирали по-нататъшното изпълнение на кода.

Блокиране срещу неблокиране

Блокирането се отнася до операции, които блокират по-нататъшното изпълнение, докато тази операция завърши, докато неблокирането се отнася до код, който не блокира изпълнението. Или както казва Node.js docs, блокирането е, когато изпълнението на допълнителен JavaScript в процеса Node.js трябва да изчака, докато не приключи операция, различна от JavaScript.

Блокиращите методи се изпълняват синхронно, докато неблокиращите методи се изпълняват асинхронно.

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

В първия пример по-горе console.logще бъде извикан преди moreWork(). Във втория пример fs.readFile()не се блокира, така че изпълнението на JavaScript може да продължи и moreWork()ще бъде извикано първо.

В Node неблокирането се отнася главно до I / O операции, а JavaScript, който показва лоша производителност поради интензивността на процесора, а не изчакването на операция, която не е JavaScript, като I / O, обикновено не се нарича блокиране.

Всички I / O методи в стандартната библиотека Node.js предоставят асинхронни версии, които не са блокиращи, и приемат функции за обратно извикване. Някои методи също имат блокиращи аналози, които имат имена, които завършват със Sync.

Неблокиращите I / O операции позволяват на един процес да обслужва множество заявки едновременно. Вместо процесът да бъде блокиран и да чака I / O операциите да завършат, I / O операциите се делегират на системата, така че процесът да може да изпълни следващата част от кода. Неблокиращите I / O операции осигуряват функция за обратно извикване, която се извиква, когато операцията приключи.

Обратни обаждания

А обратно повикване е функция предава като аргумент в друга функция, която след това може да бъде извикан (извика) във вътрешността на външната функция, за да завършите някакво действие в удобно време. Извикването може да е незабавно (синхронизиране на обратно извикване) или да се случи по-късно (асинхронно обратно извикване).

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

В първия пример функцията за обратно извикване се извиква незабавно в рамките на външната функция за поздрави и влиза в конзолата, преди да moreWork()продължи.

Във втория пример fs.readFile (асинхронен метод, предоставен от Node) чете файла и когато приключи, извиква функцията за обратно извикване с грешка или съдържанието на файла. Междувременно програмата може да продължи изпълнението на кода.

Асинхронно обратно извикване може да се извика, когато се случи събитие или когато задачата завърши. Той предотвратява блокирането, като позволява междувременно да се изпълни друг код.

Вместо процедурното четене на код отгоре надолу, асинхронните програми могат да изпълняват различни функции по различно време в зависимост от реда и скоростта, които се случват по-ранни функции като http заявки или четене на файлова система. Те се използват, когато не знаете кога ще приключи някаква асинхронна операция.

Трябва да избягвате „ ада за обратно извикване “, ситуация, при която обратните обаждания са вложени в други обратни обаждания на няколко нива, което прави кода труден за разбиране, поддържане и отстраняване на грешки.

Събития и програмирано от събития програмиране

Събитията са действия, генерирани от потребителя или системата, като щракване, завършено изтегляне на файл или хардуерна или софтуерна грешка.

Управляваното от събития програмиране е програмна парадигма, при която потокът на програмата се определя от събитията. Управлявана от събития програма изпълнява действия в отговор на събития. Когато се случи събитие, то задейства функция за обратно извикване.

Сега, нека се опитаме да разберем Node и да видим как всички те са свързани с него.

Node.js: какво е това, защо е създадено и как работи?

Просто казано, Node.js е платформа, която изпълнява сървърни JavaScript програми, които могат да комуникират с I / O източници като мрежи и файлови системи.

Когато Райън Дал създава Node през 2009 г., той твърди, че с I / O се работи неправилно, блокирайки целия процес поради синхронно програмиране.

Традиционните техники за уеб обслужване използват модела на нишката, което означава по една нишка за всяка заявка. Тъй като при I / O операция заявката прекарва по-голямата част от времето, докато я завърши, интензивните I / O сценарии включват голямо количество неизползвани ресурси (като памет), свързани с тези нишки. Следователно моделът „една нишка на заявка“ за сървър не се мащабира добре.

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.