1.41M
Category: programmingprogramming

Frontend. Лекція 56. Асинхронність у Node.js

1.

FRONTEND
Лекція 56. Асинхронність у Node.js
Використовується анімація, відображається тільки при завантаженні файлу лекції на комп'ютер

2.

Підготовка та запуск проєкту
Завантажте папку з проєктом за посиланням, проєкт матиме наступний вигляд:
Структура:
У package.json файлі встановлено тип модулів modules, а також налаштована
команда start на запуск файлу app.js.
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
}
}
Введіть команду npm i для встановлення модуля express.

3.

Підготовка та запуск проєкту
Вміст файлу app.js.
import express from 'express'
import path from 'path'
import { fileURLToPath } from 'url';
import { createRequire } from 'node:module';
import fs from 'fs';
У файлі app.js підключено модуль express, та
створено кореневий маршрут /.
а також створені змінні __filename, __dirname та
функція require.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const PORT = 3000;
const app = express();
app.get('/', (req, res)=>{
res.send('<h1>Wellcome</h1>');
})
app.listen(PORT, ()=> {
console.log(`Server started on http://localhost:${PORT}`);
})

4.

Глобальний об'єкт process
process — є глобальним об'єктом, який надає інформацію та управління поточним процесом Node.js.
Він доступний у будь-якому файлі, який виконується в контексті Node.js, і містить властивості та
методи, які можуть бути використані для керування процесом.
process.argv — зберігає масив, що містить аргументи командного рядка, передані під час запуску
програми Node.js.
process.env — є об'єктом, що містить змінні середовища, доступні в поточному процесі Node.js. Змінні
середовища є глобальними змінними, які можуть бути використані в будь-якому місці системи для
передачі інформації про конфігурації або іншої інформації, необхідної для функціонування додатків і
сервісів.

5.

Глобальний об'єкт process
process.cwd() — повертає поточний робочий каталог процесу Node.js.
process.uptime() — повертає кількість секунд з моменту запуску процесу Node.js.
process.emit() — дозволяє генерувати та відправляти події для глобального об'єкта process, який
представляє поточний процес Node.js.
process.on(event, listener) — дозволяє реєструвати обробники подій для Node.js.
process.platform — повертає операційну систему, де запущено процес Node.js.
process.exit([code]) — завершує процес Node.js із вказаним кодом повернення (за замовчуванням код
0).
Ми описали основні властивості та методи глобального об'єкта process. Більш детальна інформація доступна за посиланням.

6.

Властивість process.env.PORT, приклад
Встановимо можливість запуску нашого застосунку на різних портах. Для цього скористаємося властивістю process.env.PORT
Файл index.js:
const PORT = process.env.PORT || 3000;
Ми можемо задати порт через термінал
командою PORT=8000 npm start:
Або вказати порт у package.json файлі
Команда для Windows:
"start": "SET PORT=8000 && node index.js"
Команда для MAC або LINUX:
"start": "PORT=8000 node index.js"

7.

Властивість process.env.NODE_ENV
Властивість process.env.NODE_ENV — це змінна оточення в Node.js, яка використовується для
визначення поточного середовища виконання. Вона може приймати одне з кількох значень
"development" – використовується під час розробки програми, "production" – використовується під час
роботи програми у виробничому середовищі, "Test" – використовується для запуску тестів.
Модуль cross-env — цей сторонній модуль дозволяє використовувати єдиний синтаксис, для команд
які працюють за змінними середовища, у всіх операційних системах.
Встановимо модуль cross-env командою npm i cross-env -D:

8.

Властивість process.env.NODE_ENV, приклад
Встановимо можливість запуску нашого застосунку у production та development режимах. Для цього у файлі package.json
зробимо ще одну команду dev і також використаємо модуль cross-env для кросплатформного запуску команди:
"scripts": {
"start": "cross-env PORT=8000 NODE_ENV=production node index.js",
"dev": "cross-env NODE_ENV=development node index.js"
},
Файл index.js:
if(process.env.NODE_ENV == 'development') {
console.log('development mode');
} else {
console.log('production mode');
}
Тепер в залежності від режиму ми можемо
застосовувати різні інструкції до нашого застосунка.

9.

Властивість process.env.NODE_ENV, результат
Команда npm start:
Команда npm run dev:

10.

Потік
Потік stream — це абстрактна ідея передачі даних у вигляді послідовності байт (або об'єктів) між
джерелом даних та їх призначенням. JavaScript є одно поточною мовою програмування, але Node.js
працює з бібліотекою libuv, яка компілює код написаний на JavaScript, в код написаний на мові C та
C++ в яких існує можливість створення декількох потоків.
Одно поточність JavaScript — передбачає що весь код має відбуватися синхронно, тобто кожна
наступна інструкція коду повинна чекати на виконання попередньої інструкції. Якщо виконання певної
інструкції займає багато часу, наприклад ми читаємо великий файл, то в одно поточній моделі програма
зависне оскільки їй необхідно дочекатися виконання попередньої інструкції для того щоб продовжити
йти далі. Для вирішення цієї проблеми JavaScript використовує спеціальний механізм – event loop, а у
Node.js впроваджені спеціальні модулі для роботи з потоками – worker_threads.
Увага! event loop має різні механізми реалізації у браузері та у Node.js, далі ми розглянемо його структуру саме у Node.js.

11.

Еvent loop
Цикл події event loop — це безкінечний цикл, який очікує подій, обробляє їх та повертається до
очікування. Коли Node.js запускається, створюється головний процес, який відповідає за виконання
JavaScript коду. Event loop – це частина цього процесу, яка працює в фоновому режимі та обробляє
події від інших частин системи, таких як вхідні/вихідні потоки, заплановані таймери, мережеві запити
тощо.
Всі інструкції у JavaScript потрапляють у стек виклику, після чого вони виконуються по черзі у стеці,
якщо подія, яка має виконуватися, є асинхронною, наприклад таймер який спрацює через 1 секунду, ці
виклики переносяться до циклу подій event loop який на кожній своїй ітерації буде перевіряти настання
події (чи пройшла 1 секунда) і якщо так, то викликати callback функцію.
Цикл події event loop — складається з 6 фаз. На кожній фазі йде перевірка закінчення процесів. Якщо
процес закінчено, то буде виконано callback функцію, яка була передана, якщо ні цикл йде перевіряти
наступні фази і повернеться до перевірки цього процесу на наступній ітерації.

12.

Фази циклу Еvent loop
1) Timers — у цій фазі Event Loop обробляє таймери, додані через функції, такі як setTimeout та
setInterval. Якщо час таймера минув, його зворотний виклик додається в чергу pending callbacks.
2) pending callbacks — у цій фазі Event Loop виконує зворотні виклики, які були додані в чергу через
функції, такі як setTimeout або setImmediate. Зворотні виклики можуть бути додані в чергу з будь-якої
іншої фази.
3) idle, prepare — ця фаза призначена для внутрішнього використання самою системою Node.js.

13.

Фази циклу Еvent loop
4) poll — у цій фазі Event Loop очікує ввід-вивід (наприклад, мережевих запитів або відповіді на них)
або обробляє з'єднання з базою даних. Якщо є події в черговому вводі-виводі, Event Loop обробляє їх.
5) check — у цій фазі Event Loop виконуються зворотні виклики, додані через функцію setImmediate.
6) close callback — у цій фазі Event Loop виконує зворотні виклики, які були додані через функції, такі
як socket.on('close', ...) або server.on('close', ...). Ця фаза виконується, коли всі з'єднання закриті.
Як Ви могли помітити в фазах event loop відсутні promise.
Promise виконуються або перед першою фазою або після кожної фази циклу event loop.

14.

Еvent loop, схема
Ініціалізація
timers
pending callbacks
idle, prepare
poll
check
close callback
Вихід?
Promise

15.

Еvent loop, приклад
console.log('start 1');
Результат
setImmediate( ()=> console.log('setImmediate 2'))
setTimeout( ()=>{
console.log('setTimeout 3')
Promise.resolve(console.log('Promise in setTimeout 4'));
}, 0)
setImmediate( ()=> {
setTimeout( ()=>{console.log('setTimeout in setImmediate 5')}, 0)
console.log('setImmediate 6')
Promise.resolve(console.log('Promise in setImmediate 7'))
})
Promise.resolve(console.log('Promise 8'));
console.log('end 9');
Зверніть увагу як змінюється порядок
виведення відповідей у консолі. Цей вивід
обумовлено циклом виконання event loop.

16.

Еvent loop, анімований приклад
Результат
timers
pending callbacks
idle, prepare
poll
Stack
check
close callback
console.log('start 1');
setImmediate( ()=> console.log('setImmediate 2') )
setTimeout( ()=>{console.log('setTimeout 3') , 0)
console.log('end 4');
Натискайте ліву кнопку миші щоб програвати
анімацію на цьому слайді

17.

Концепція callback
Концепція callback — є одним із фундаментальних аспектів програмування на Node.js. Вона дозволяє
асинхронно виконувати операції введення/виведення та обробку подій у Node.js, без блокування
основного потоку виконання.
В Node.js — більшість операцій є асинхронними, що означає, що функція, яка ініціює операцію,
завершується до її завершення. Це дозволяє іншій операції виконуватися в той час, коли очікується
завершення початкової операції. Коли початкова операція завершується, Node.js викликає функцію
зворотного виклику, щоб повідомити про результат операції.
Функція callback — являє собою функцію, яка передається в якості аргументу іншої функції і
викликається після завершення операції. Вона зазвичай приймає два аргументи: перший аргумент
містить інформацію про помилку, якщо така виникла, другий аргумент містить результат виконання
операції.

18.

Концепція callback
При використанні вбудованих модулів ви можете помітити що деякі з них закінчуються на «Sync»,
Наприклад: fs.readFile та fs.readFileSync або fs.writeFile та fs.writeFileSync.
Методи з «Sync» (fs.readFileSync / fs.writeFileSync) — приймають один аргумент (шлях до файлу) і
працюють синхронно блокуючи потік виконання. Тобто наступна інструкція коду не буде виконана, поки
не буде прочитано / записано файл.
Методи без «Sync» (fs.readFile / fs.writeFile) — працюють асинхронно, та не блокують основний потік.
Вони приймають другим аргументом callback функцію яка буде виконана після завершення читання /
запису файлу.

19.

Концепція callback, приклад
Для прикладу прочитаємо package.json файл методом fs.readFile.
console.log('start');
fs.readFile(path.join(__dirname, './package.json'), (err, content) => {
if (err) {
console.error(err);
}
console.log(content.toString());
});
console.log('end');
Як бачимо метод fs.readFile не заблокував потік виконання, і код
продовжив виконуватися далі, а коли файл було прочитано
спрацював callback та вивів дані у консоль.

20.

Promise
Ми можемо використовувати Promise для досягнення асинхронного виконання коду:
console.log('start');
function readFilePromise(filePath) {
return new Promise((resolve, reject) => {
try{
const data = fs.readFileSync(filePath);
resolve(data.toString())
} catch(error){
reject(error)
}})}
readFilePromise(path.join(__dirname, './package.json'))
.then(data => {console.log(data)})
.catch(err => {console.error(err)});
console.log('end');
Зверніть увагу що ми використовуємо синхронну функцію
fs.readFileSync але завдяки тому що вона обгорнута у promise
досягається асинхронне виконання.

21.

Promise,пояснення коду
Код містить функцію readFilePromise, яка повертає Promise об'єкт,
який читає вміст файлу за вказаним шляхом filePath за допомогою
методу fs.readFileSync() і повертає його вміст у вигляді рядка.
function readFilePromise(filePath) {
return new Promise((resolve, reject) => {
try{
const data = fs.readFileSync(filePath);
resolve(data.toString())
} catch(error){
reject(error)
}})}
readFilePromise(path.join(__dirname, './package.json'))
.then(data => {console.log(data)})
.catch(err => {console.error(err)});
Об'єкт Promise містить два колбека - resolve та reject, які
викликаються в залежності від результату виконання функції
fs.readFileSync(). Якщо читання файлу відбулося успішно,
викликається resolve з аргументом, що містить файл у вигляді
рядка. Якщо виникла помилка, то викликається reject з
аргументом, що містить інформацію про помилку.
Потім функція readFilePromise викликається з передачею шляху до
файлу в якості аргументу. Якщо читання файлу відбувається
успішно, керування передається в блок then, де виводиться вміст
файлу. Якщо читання файлу відбулося невдало, керування
передається до блоку catch, де виводиться інформація про
помилку.

22.

Async та Await
Для зручної роботи з promise модуль fs пропонує окремий об'єкт promises який містить ті ж самі методи (readFile, writeFile
тощо), але повертає promise. Імпортуємо цей об'єкт:
import fs, { promises}
Спробуємо прочитати файл використовуючи async:
console.log('start');
const filePath = path.join(__dirname, './package.json')
console.log((await promises.readFile(filePath)).toString());
console.log('end');
from 'fs';
Як бачимо такий код виконується синхронно:

23.

Async та Await
Для досягнення асинхронності потрібно створити функцію:
const filePath = path.join(__dirname, './package.json’)
async function getData () {
const fileData = await promises.readFile(filePath)
console.log(fileData.toString());
}
console.log('start');
getData ();
console.log('end');

24.

Як виконувати асинхронний код
Отже, у Node.js асинхронний код виконується за допомогою механізму callback функцій, promise та асинхронних функцій.
Функції callback — це функції, які передаються як аргументи в інші функції та викликаються після
завершення операції.
promise — це об'єкти, які представляють результат асинхронної операції та дозволяють виконувати дії
після її завершення.
Асинхронні функції — це функції, які використовують ключове слово async і повертають проміси.
Всередині таких функцій можна використовувати ключове слово await, щоб очікувати на виконання
асинхронної операції.

25.

Worker threads
Worker threads — використовуються для паралельної обробки обчислювально-інтенсивних завдань.
Вони є механізмом, який дозволяє виконувати код в окремих потоках, які можуть працювати
паралельно на багатоядерних процесорах. Кожен worker threads працює у власному контексті, що
дозволяє уникнути блокування і знижує ймовірність помилок.
Worker threads — використовується для створення окремих потоків на окремих ядрах процесору,
кожне ядро процесору може бути використане для створення окремого потоку. За замовчанням
кількість worker threads встановлено 4, але її можна збільшити до 1024. При збільшенні або зменшенні
кількості worker threads потрібно орієнтуватися на кількість ядер у процесорі, оскільки всі worker
threads яким не вистачило ядра, просто будуть чекати своєї черги виконання і не дадуть ніякого
приросту швидкості.

26.

Worker threads, схема
Main Thread
Worker Thread
JavaScript
C++
Виклик функції
Розпакування JavaScript об'єктів
Відправка задачі
Виконання задачі
Обробка запиту
Продовження роботи
Повернення до JavaScript коду
Виклик callback функції
Пакування JavaScript об'єктів
Закінчення виконання

27.

Worker threads — метод worker.on
worker.on — використовується для прослуховування різних подій, які відбуваються у worker thread у
Node.js. Цей метод приймає два параметри: назву події (message, error або exit) та функцію
обробника, яка буде викликана, коли подія відбудеться.
Подія message — відбувається, коли у worker thread надходить повідомлення від основного потоку,
відправлене за допомогою методу worker.postMessage().
Подія error — відбувається, коли у worker thread відбувається помилка.
Подія exit — відбувається, коли worker thread завершує свою роботу.

28.

Worker threads — об'єкти workerData, parentPort
Об'єкт workerData — це об'єкт, який передається у конструктор Worker під час створення worker
thread. Він дозволяє передавати дані з основного потоку в worker thread. Дані, передані через
workerData, доступні в worker thread як властивості workerData об'єкта worker, створеного всередині
worker thread.
Об'єкт parentPort — це об'єкт, який є портом, через який worker thread може спілкуватися з основним
потоком. Цей об'єкт доступний лише всередині worker thread. Порт дозволяє відправляти
повідомлення з worker thread до основного потоку за допомогою методу parentPort.postMessage().

29.

Worker threads, приклад
Імпортуємо клас Worker з модулю worker_threads:
import { Worker } from 'worker_threads';
Створимо об'єкт worker та опишемо методи для прослуховування подій
const worker = new Worker('./worker.js', { workerData: 'ABCDEFG' });
worker.on('message', (msg) => {
console.log(`Answer: ${msg}`);
});
worker.on('error', (err) => {
console.error(err);
});
worker.on('exit', (code) => {
console.log(`Worker finished with code: ${code}`);
});
Рядок new Worker('./worker.js', { workerData: 'ABCDEFG' }) говорить про те що
ми будемо використовувати файл worker.js і передавати в нього об'єкт з
ключем workerData та значенням 'ABCDEFG'.

30.

Worker threads, приклад
Створимо файл
worker.js:
У файлі worker.js імпортуємо об'єкти workerData та parentPort з модулю worker_threads:
import { workerData, parentPort } from 'worker_threads';

31.

Worker threads, приклад
У файлі worker.js створимо код який буде повертати рядок у зворотному порядку:
import { workerData, parentPort } from 'worker_threads';
const reversed = workerData.split('').reverse().join('');
parentPort.postMessage(reversed);
Результат:
English     Русский Rules