Node.js: що це таке, коли і як ним користуватися, і чому ви повинні це робити

Напевно, ви вже читали ці речення раніше ...

Node.js - це середовище виконання JavaScript, побудоване на JavaScript-двигуні V8 Chrome Chrome. Node.js використовує керований подіями асинхронний неблокуючий ввід / вивід modelNode.js працює на одному циклі події циклу

… І залишилися здивованими, що все це означає. Сподіваємось, до кінця цієї статті ви краще зрозумієте ці терміни, а також те, що таке Node, як він працює, а також чому і коли є гарною ідеєю використовувати його.

Почнемо з перегляду термінології.

I / O (вхід / вихід)

Коротко від вводу / виводу, введення / виведення насамперед відноситься до взаємодії програми з диском та мережею системи. Приклади операцій вводу-виводу включають читання / запис даних з / на диск, надсилання HTTP-запитів та розмову з базами даних. Вони дуже повільні в порівнянні з доступом до пам'яті (ОЗУ) або виконанням робіт на центральному процесорі.

Синхронний проти асинхронного

Синхронне (або синхронізоване) виконання зазвичай відноситься до коду, що виконується послідовно. При програмуванні синхронізації програма виконується рядок за рядком, по одному рядку. Кожного разу, коли викликається функція, виконання програми чекає, поки ця функція повернеться, перш ніж переходити до наступного рядка коду.

Асинхронне (або асинхронне) виконання відноситься до виконання, яке не виконується в тій послідовності, яка відображається в коді. При асинхронному програмуванні програма не чекає завершення завдання і може перейти до наступного завдання.

У наступному прикладі операція синхронізації змушує попередження спрацьовувати послідовно. В операції асинхронізації, хоча попередження (2) виконується другим, це не відбувається.

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

Операція асинхронізації часто пов’язана з введенням-виведенням, хоча setTimeoutє прикладом чогось, що не є введенням-виведенням, але все ще є асинхронним. Взагалі кажучи, все, що пов'язано з обчисленнями, є синхронізацією, а все, що пов'язане з введенням / виходом / синхронізацією, є асинхронізацією. Причиною того, що операції введення-виведення виконуються асинхронно, є те, що вони дуже повільні і в іншому випадку можуть блокувати подальше виконання коду.

Блокування проти неблокування

Блокування відноситься до операцій, які блокують подальше виконання, доки ця операція не закінчиться, тоді як неблокування стосується коду, який не блокує виконання. Або як кажуть документи Node.js, блокування - це коли додаткове виконання 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 неблокування насамперед відноситься до операцій вводу-виводу, а JavaScript, який демонструє низьку продуктивність через інтенсивність процесора, а не очікування на операції, що не пов'язана з JavaScript, наприклад I / O, зазвичай не називають блокуванням.

Усі методи вводу-виводу в стандартній бібліотеці Node.js надають асинхронні версії, які не блокуються, та приймають функції зворотного виклику. Деякі методи також мають блокуючі аналоги, які мають імена, які закінчуються синхронізацією.

Неблокувальні операції вводу-виводу дозволяють одному процесу обслуговувати кілька запитів одночасно. Замість того, щоб процес блокувався і чекав завершення операцій вводу-виводу, операції вводу-виводу делегуються системі, щоб процес міг виконати наступний фрагмент коду. Неблокувальні операції вводу-виводу забезпечують функцію зворотного виклику, яка викликається після завершення операції.

Зворотні дзвінки

Зворотний викликом є функція передається в якості аргументу в іншу функцію, яка потім може бути викликана (відкликано) всередині зовнішньої функції для завершення якого - яких дій у зручний час. Виклик може бути негайним (синхронізація зворотного виклику) або це може статися пізніше (асинхронний зворотний виклик).

// 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, які можуть взаємодіяти з джерелами вводу-виводу, такими як мережі та файлові системи.

Коли Райан Даль створив Node у 2009 році, він стверджував, що введення / виведення оброблялося неправильно, блокуючи весь процес через синхронне програмування.

Традиційні методи веб-сервісу використовують модель потоку, тобто одну нитку для кожного запиту. Оскільки в операції вводу-виводу запит витрачає більшу частину часу в очікуванні завершення, інтенсивні сценарії введення-виведення спричиняють велику кількість невикористаних ресурсів (таких як пам’ять), пов’язаних з цими потоками. Тому модель «один потік на запит» для сервера погано масштабується.

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.