1

Тема: Патерни + Node.js

Почав читати книжку по nodejs та патернам проєктування. І думаю - просто прочитати її недостатньо, потрібно зрозуміти. Найкраще для того, аби зрозуміти - це не тільки читати, але й робити. Але в книзі є багато таких концепцій, реалізовувати котрі не є логічно, бо мені то не треба буде писати власноруч, але зрозуміти то всьо варто, тому я подума - а що, якщо перечитавши певний розділ книги, я спочатку перепочину кілька хвилин, а потім, з трохи свіжою головою спробую пригадати всьо прочитане. Мені здається, такий підхід дуже класний, тому що спочатку я то читаю і розумію, а потім я то всьо пригадую, і цим воно краще закріплюється в пам'яті.
Проблема тільки в тому, що робити то в своїх думках не дуже цікаво, тому я вирішив, що буду описувати вивчені речі тут, ніби пояснюючи вам, і воно має допомогти мені:
А) краще зрозуміти та запам'ятати прочитане
Б) якщо я щось не так зрозумію, то ви це помітите, і виправите мене, хехехехе

2

Re: Патерни + Node.js

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

Аби пофіксити це, люди вигадали дуже просте рішення - а давайте будемо створювати по потічку на кожного користувача/ресурс, тоді операції, що стосуються окремих користувачів будуть виконуватись в окремих потічках, паралельно, і не будуть блокувати одне одного.

Такий підхід працює, але блокуючі операції можуть займати дуже багато часу, і під час цього часу нічого не відбувається, тому виходить так, що кожен потічок за всю тривалість свого життя може дуже багато часу просто стояти без діла, і в ньому нічого не відбуватиметься, але при цьому кожен потічок вимагає значних ресурсів, як от оперативної пам'яті, тому з таким підходом дуже багато ресурсів використовується на простоювання.

Яке наступне можливе рішення? - Перевірка статуса ресурсу!

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

Тепер, якщо в нас є декілька таких сокетів, то ми можемо створити вічний цикл, котрий перевіряв би значення поля STATUS в кожному сокеті, і якщо це поле повідомляє нас про те, що ми можемо прочитати дані - то ми їх читаємо і обробляємо, після чого цикл продовджується.

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


І тепер ми наблизились до того, як подібні речі працюють в NodeJs.

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

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

Псевдокод може виглядати ось так

while (events = OS.pleaseGiveEvents()) { // блокуюча операція
   forEach(event in events) { // проходимось по масиву з повідомленнями
    handleEvent(event); // і обробляємо кожне повідомлення
  }
}

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

Подібна обробка повідомлень/подій називається event demultiplexing - демультиплексія подій, суть її в тому, що ми отримуємо повідомлення з різних місць (різних ресурсів), але обробляємо їх в одному місці.
А патерн проектування, що використовує цю фічу, називається Реактор.  https://en.wikipedia.org/wiki/Reactor_pattern
А в NodeJs ця фіча називається event loop (але то всьо спрощено, звісно ж).
https://static.packt-cdn.com/products/9781783287314/graphics/7314OS_01_02.jpg

3

Re: Патерни + Node.js

Як виявилося, я помилився щодо того демультиплексора та патерна Реактор.
Демультеплексором є саме та вбудована в ОС фіча, котра обробляє неблокуючі операції, та "відправляє" повідомлення, коли якась неблокуюча операція завершена. Тоді це повідомлення (разом з обробником отриманих даних) потрапляє в чергу, після чого, в циклі подій, ми витягуємо ті повідомлення та викликаємо обробник передаючи в них дані. Коротше, там трішки складнувато підібрати правильні терміни, бо користувацький код, фіча самої ОС та кишки NodeJs працюють разом, і вся ця купа речей являють собою патерн Реактор.
https://i.stack.imgur.com/GtSae.jpg

4

Re: Патерни + Node.js

Прочитав був про те, як відбувається імпортування модулів в Node.Js з використанням функції require.

Виявляється, що коли ми імпортуємо модуль

const myModule = require('./myModule');

То всередині функції створюєтся об'єкт

const module = {
 exports: {},
 id
}

Як бачите, цей об'єкт містить два поля - exports та id
перше поле міститиме все те, що ми експортуємо з модуля, а друге є ідентифікатором модуля, аби ми потім змогли знайти його в кеші.
Далі цей об'єкт module відправляється аргументом в функцію, котра містить користувацький код, і всередині цієї функції об'єкт module.exports наповнюється тими речами, які ми хочемо експортувати

// файл myModule.js
module.exports = { name: 'FakiNyan', sayHi: function() { console.log(`Hi, ${this.name}`) } };

І тепер, в іншому файлі я зможу імпортувати цей модуль, та виконати функцію sayHi наступним чином

const myModule = require('./myModule');
myModule.sayHi(); // виведе "Hi, FakiNyan"

Але ж ви розумієте, що користувацький модуль myModule.js може також спробувати імпортувати інші модулі, і для цього йому потрібна функція require, тому вона також передається в функцію разом з об'єктом module.

При цьому в коді файлу myModule.js ми не бачимо ніякої функції, котра б отримувала якийсь require та module як аргументи, але вона є!
Це відбувається автоматично, і це можна легко довести, якщо в файлі myModule.js додати код

console.log(arguments);

arguments - це масив аргументів, котрі передаються в функцію, він явно не описується як аргумент функції, але він є.

console.log вище виведе нам інформацію про аргументи, котрі неявно передались в функцію, котра так само неявно та автоматично огорнула код нашого модуля.
Перший аргумент - це об'єкт module.exports, другий аргумент - функція require, третій аргумент - просто об'єкт module (так, в функцію передається як поле об'єкту module - exports, так і сам об'єкт module), а наступними аргументами будуть шлях до файлу модуля, та шлях до директорії, в котрій знаходиться модуль.

5

Re: Патерни + Node.js

Тепер я опишу ті правила тезисно.

  1. Кожен модуль може імпортувати інші модулі, використовуючи функцію require

  2. Функція require працює синхронно, тому коли в коді зустрічається require, контроль передається в неї, і коли контроль повертається - виконується решта коду в модулі, в котрому ми викликали require

  3. Під час виконання коду в модулі ми можемо наповнювати об'єкт module.exports різними полями з різними значеннями, і то всьо (саме об'єкт exports) буде повернено з функції require

  4. Також функція require кешує імпортовані модулі, тому лише при першому імпорті модуля код всередині нього буде виконано, наступні спроби імпортувати цей модуль будуть просто повертати об'єкт exports з кешу.

6

Re: Патерни + Node.js

А тепер найцікавіше... Два модулі можуть імпортувати один одного, таким чином утворюється цикл.
Нехай маємо модуль a.js з наступним кодом

exports.loaded = false;
const b = require('./b'); 
module.exports = {
  bLoaded: b.loaded,
  loaded: true,
};

І модуль b.js з таким кодом

exports.loaded = false;
const a = require('./a');
module.exports = {
  aLoaded: a.loaded,
  loaded: true,
};

Увага, питання.
Що виведеться в консолі, якщо ми виконаємо наступний код.

let a = require('./a');
let b = require('./b');

console.log(a);
console.log(b);

7

Re: Патерни + Node.js

уідповідь

Спершу ми імпортуємо модуль a.
Контроль передається в файлик a.js, і спочатку код модуля встановлює

exports.loaded = false;

тобто, на даний момент модуль a всього лише імпортує змінну loaded зі значенням false.
Після цього код імпортує модуль b, відповідно, контроль передається в файлик b.js. В цьому файлі ми також встановлюємо

exports.loaded = false;

, після чого імпортуємо модуль a.
Що ж відбувається тепер?
Всередині require відбувається кешування модуля, і це відбувається відразу, ще перед тим, як код модуля виконується повністю. Таким чином, на цей момент модуль a вже закешовано, і require всередині модуля b поверне нам об'єкт exports, котрий на цей момент складається з

{loaded: false}

.
Після цього код всередині модуля b поверне

{ aLoaded: false, loaded: true }

це і буде тим, що експортує модуль b, і ця інформація закешується.

Далі ми повертаємось назад в модуль a, і повертаємо з модуля

{ bLoaded: true, loaded: true }

.

Тому console.log(a) покаже

{ bLoaded: true, loaded: true }

Тепер поговоримо про те, що виведе console.log(b);

Коли ми імпортуємо модуль b, відбувається наступне:
а нічого особливо і не відбувається, бо коли ми викликали модуль a, той, всередині себе імпортував модуль b, тому цей модуль вже закешований, і exports цього модуля дорівнює рівному тому ж, чому він і дорівнював під час першого експорту модуля b  всередині модуля a,  а саме 

{ aLoaded: false, loaded: true }

Тому кінцевий результат, котрий побачимо в консолі

{ bLoaded: true, loaded: true }
{ aLoaded: false, loaded: true }

8

Re: Патерни + Node.js

хамовитому модератору

могли хоча б повідомити мене про перенесення!!1

9

Re: Патерни + Node.js

Напишу ще декілька цікавинок про модулі та пов'язані з ними речі.

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

require('./a');
console.log(require.cache);

Цей кеш - звичайний об'єкт, ключі котрого є повним шляхом до файлів модуля, а значеннями є самі модулі. Тому якщо ви хочете, аби наступне імпортування модуля відбувалось з завантаженням та виконанням коду модуля, а не просто через повернення закешованих даних, потрібно просто присвоїти певному ключу на об'єкті require.cache значення undefined.

Щоб отримати ключ можна використати іншу функцію, котра є властивістю функції require - resolve.

let a = require('./a'); // імпортуємо модуль a
a = require('./a'); // отримуємо результат з кешу
const id = require.resolve('./a'); // отримуємо повний шлях (id) до файлу модуля
require.cache[id] = undefined; // знищуємо дані про модуль в кеші

a = require('./a'); // код модуля знову завантажується та виконується, як вперше

10

Re: Патерни + Node.js

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

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

calculate

. Модуль можна розглядати як велику чорну коробку, в котрій багато механізмів, а цю частину

module.exports = {
  calculate,
};

як кнопку на цій коробці, натискаючи на котру механізми в коробці починають щось робить, таку кнопку ще можна назвати "інтерфейсом", або щ API - application programming interface.
Використовується даний модуль наступним чином.

const a = require('./a');

console.log(a.calculate(2, 3)); // Result is:  5

Погодьтесь, дуже просто, але навіть тут нам потрібно знати, які саме поля будуть міститись на тому об'єкті, що повертає модуль (в цьому випадку цей об'єкт зветься a, а єдине поле на ньому - calculate, котер є функцією).

Подібний модуль можна спростити. Наступний патерн називається на честь людини, що створила його substack.

Ідея в тому, аби повертати якомога менше інформації з модуля, і аби вона була в такому форматі, котрий дозволяє легко та швидко використовувати ОСНОВНИЙ функціонал модуля.

// модуль addition.js
const message = 'Result is: ';
const addition = (a, b) => a + b;
const format = (a, b) => `${message} ${addition(a, b)}`;

addition.format = format;

module.exports = addition;
// головний файл
const addition = require('./addition');

console.log(addition(2, 3));
console.log(addition.format(2, 3));

Як бачите, я змінив назву модуля, аби вона відповідала функціоналу цього модуля, так набагато легше розуміти, що воно має робить, і як ним користуватись.
При цьому з самого модуля я відразу експортую головну функцію - addition, а не об'єкт з полем addition, значення котрого було б тією функцією.
Тепер користувачу не треба думати про поля, а треба просто відразу виконати функцію, і отримати результат.
Я також змінив ім'я функції calculate на format, тому що це ім'я більше відповідає тому, що вона робить, та додав цю функцію на функцію addition. Таким чином користувач відразу має доступ до основного функціоналу модуля, а якщо йому треба більше, то він все одного може використати функцію format, але для цього вже треба зазирнути в документацію.

11

Re: Патерни + Node.js

Також існує можливість створення модулів, котрі містять в собі певний стан.

// модуль saidHi.js
function saidHi() {
  this.count = 0;
}

saidHi.prototype.sayHi = function () {
  this.count++;
  console.log(`Said hi ${this.count}`);
};

module.exports = new saidHi();
// модуль mod.js
const saidHi = require('./saidHi');

saidHi.sayHi();
saidHi.sayHi();
// головний файл
require('./mod');
const saidHi = require('./saidHi');

saidHi.sayHi();
saidHi.sayHi();

Результатом буде

Said hi 1
Said hi 2
Said hi 3
Said hi 4

В модулі saidHi ми створюємо об'єкт типу saidHi, та повертаємо його як значення самого модуля. Цей об'єкт містить функцію sayHi, котра змінює свій внутрішній стан - count, та логує значення цієї змінної. Цей стан тепер спільний для головного файлу та модуля mod.js. Хоча ми й імпортуємо модуль saidHi окремо, в головному файлі та в mod.js (привіт кеш).

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

12

Re: Патерни + Node.js

Додам ще, що модулі можуть засирати глобальний простір та змінювати (патчити, або ж латати) інші модулі, це зветься monkey patching, але такого краще не робити (хіба що під час тестування може бути корисною ця фіча)

13

Re: Патерни + Node.js

Цей во. Почитав був трішечки про те, як обробляються асинхронні дії в ноді.

Так, як асинхронна дія забирає певний час, та закінчується колись в майбутньому, то ми не можемо просто отримати результат, і щось з ним робити, як тут

const result = asyncFunction();
console.log(result);

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

asyncFunction( result => console.log(result) ); // коли asyncFunction закінчить свою роботу, вона викличе цю анонімну функцію, передаючи в неї результат виконання, якщо такий є. Далі анонімна функція просто консоль.логне той результат.

Такий підхід є дуже простим та працюючим, коли в нас є один результат, котрий потрібно просто обробити якось. А що, якщо у нас може бути декілька різних результатів, котрі повинні оброблятись по-різному?
Ми можемо передавати декілька колбеків в основну функцію, і та якось повинна вирішувати. який з тих колбеків викликати, але погодьтесь, це не дуже зручно, бо такий код виглядав би якось так.

function asyncFunction(callback1, callback2, ...) {
  setTimeout(() => {
   if (...) callback1('hi');
   else if (...) callback2('bye');
   else if ....
}, 100); // через 100 мілісекунд ми спробуємо викликати якийсь з колбеків, базуючись на якихось умовах
}

За умови, що колбеків може бути нескінченна кількість нам треба буде писати купу коду, це все буде дуже важко читати та підтримувати.
Для цього є наступний патерн - Спостерігач (Observer).

Дякуючи не знаю кому, в NodeJs цей патерн вже вбудований та реалізований класом EventEmitter, що знаходиться в одному з основних модулів - events.
Він дозволяє зробити наступне:
1. Відправляти "події", котрі можна уявити листами, котрі мають якусь адресу, та щось всередині. Це може бути 100 гривнів, або лист від пана Дадаїста, в котрому він розповідає про знайомих наркоманів.
2. Реєструвати "слухачів подій", котрих можна уявити у вигляді реальних людей з реальними адресами, на котрі ті листи можуть приходити, і далі люди можуть щось з ними зробити - придбати шоколадку, якщо вони отримали гроші, або поставити в рамку, якщо це розповідь про наркоманів.

const { EventEmitter } = require('events');

function asyncFunction() {
  const emitter = new EventEmitter();
  setTimeout(() => {
    console.log('--> Відправляємо 100 гривнів');

    emitter.emit('адреса 1', '100 грн.');

    console.log('--> Відправляємо листа.');

    emitter.emit(
      'адреса 2',
      `цей во, знав я одного наркомана-шизофреніка,
         який жив неподалік від мене в закинутому заводі...`
    );
  });
  return emitter;
}

asyncFunction()
  .on('адреса 1', data =>
    console.log(`???? На адресу 1 прийшов лист, всередині нього: ${data}`)
  )
  .on('адреса 2', data =>
    console.log(`???? На адресу 2 прийшов лист, всередині нього: ${data}`)
  );
--> Відправляємо 100 гривнів
???? На адресу 1 прийшов лист, всередині нього: 100 грн.
--> Відправляємо листа.
???? На адресу 2 прийшов лист, всередині нього: цей во, знав я одного наркомана-шизофреніка,
         який жив неподалік від мене в закинутому заводі...

Дана функція використовує функцію setTimeout, котра приймає колбек, котрий виконується через певну кількість часу, тобто, асинхронно.
В колбеці я створюю екземпляр класу EventEmitter.
Метод emit дозволяє відправляти повідомлення. Першим аргументом є адреса, а другий те, що ми хочемо відправити.
Далі я повертаю цей екземпляр класу EventEmitter з функції, і відразу використовую метод on - це ще один метод класу EventEmitter, він дозволяє зареєструвати нам адресу,  та ту дію, котра виконуватиметься, коли на цю адресу прийде лист.

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

На кожну адресу/ідентифікатор можна реєструвати багацько "слухачів" за допомогою того метода - on. Хоч нуль, хоч один, хоч з десяток другий. Так само і метод emit дозволяє відправляти скільки завгодно повідомлень.

Подякували: Chemist-i1

14

Re: Патерни + Node.js

Ще трішки розповім про те, як обробляються помилки в колбеках та EventEmitter'ах.

Як ви зрозуміли, оті колбеки виконуються деінде, але точно не в тому ж контексті, що й та функція, котра його викликатиме.
Тому подібний код не обробить помилку.

function asyncFunction() {
  setTimeout(() => {
    throw Error('Дідько!');
  });
}

try {
  asyncFunction();
} catch (err) {
  console.log(`Ми отримали помилку: ${err.message}`);
}

а покаже огидне повідомлення

/home/fakinyan/node/modules/app.js:28
    throw Error('Дідько!');
    ^

Error: Дідько!
    at Timeout._onTimeout (/home/fakinyan/node/modules/app.js:28:11)
    at listOnTimeout (node:internal/timers:555:17)
    at processTimers (node:internal/timers:498:7)

Аби перехопити цю помилку, нам потрібно огорнути в try catch безпосередньо саму функцію, в котрій виникає помилка.
При цьому в середовищі nodejs є загальноприйнатим створювати колбеки такими, аби першим аргументом, що вони приймають, була саме помилка, а далі вже результат роботи якоїсь функцію.

function asyncFunction(callback) {
  setTimeout(() => {
    try {
      throw Error('Дідько!');
    } catch (err) {
      callback(err);
    }
  });
}

asyncFunction((err, result) => {
  if (err) console.log(`Ми отримали помилку: ${err.message}`);
});
Ми отримали помилку: Дідько!

Як бачите, тепер помилка файно обробилась.

*****************************

В EventEmitter'ах все працює трішки інакше. Ми не передаємо помилку першим аргументом в колбек, бо не маємо колбеку, натомість ми відправляємо помилку за адресою "error"

function asyncFunction() {
  const emitter = new EventEmitter();
  setTimeout(() => {
    try {
      throw Error('Дідько!');
    } catch (err) {
      emitter.emit('error', err);
    }
  });
  return emitter;
}

asyncFunction().on('error', err =>
  console.log(`Ми отримали помилку (EventEmitter): ${err.message}`)
);
Ми отримали помилку (EventEmitter): Дідько!

Але обов'язковою умовою є створення "отримувача" помилки, що відправляється за адресою "error". Якщо його не буде, то така помилка ніяк не обробиться, та ми знову отримаємо огидне повідомлення в консолі, і додаток перестане працювати.

function asyncFunction() {
  const emitter = new EventEmitter();
  setTimeout(() => {
    try {
      throw Error('Дідько!');
    } catch (err) {
      emitter.emit('error', err);
    }
  });
  return emitter;
}

asyncFunction();
node:events:304
      throw er; // Unhandled 'error' event
      ^

Error: Дідько!
    at Timeout._onTimeout (/home/fakinyan/node/modules/app.js:30:13)
    at listOnTimeout (node:internal/timers:555:17)
    at processTimers (node:internal/timers:498:7)
Emitted 'error' event at:
    at Timeout._onTimeout (/home/fakinyan/node/modules/app.js:32:15)
    at listOnTimeout (node:internal/timers:555:17)
    at processTimers (node:internal/timers:498:7)
Подякували: Chemist-i1

15 Востаннє редагувалося Droid 77 (30.06.2021 10:56:30)

Re: Патерни + Node.js

FakiNyan написав:

тому я подума - а що, якщо перечитавши певний розділ книги, я спочатку перепочину кілька хвилин, а потім, з трохи свіжою головою спробую пригадати всьо прочитане. Мені здається, такий підхід дуже класний, тому що спочатку я то читаю і розумію, а потім я то всьо пригадую, і цим воно краще закріплюється в пам'яті.

На мою думку не вдала ідея. Бо якщо щось не вірно зрозумів та запам'ятав, в подальшому будуть виникати машинальні помилки. Мабуть тре одразу практикуватись, про відпочинок теж слід пам'ятати. Наприклад. Годину - півтори працюєш, 15-20 хвилин відпочинок.

FakiNyan написав:

тому я вирішив, що буду описувати вивчені речі тут, ніби пояснюючи вам, і воно має допомогти мені:
А) краще зрозуміти та запам'ятати прочитане
Б) якщо я щось не так зрозумію, то ви це помітите, і виправите мене, хехехехе

Це дієвий метод, метод каченя. Коли намагаєшся пояснити буд кому що треба зробити, приходить розуміння чого ще тре повчити та зрозуміти.

16

Re: Патерни + Node.js

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

Уявімо, що нам потрібно виконати декілька речей по черзі, тобто, спочатку виконується перша функція, потім друга, а потім третя. Такий код може виглядати наступним чином.

const tasks = ['Прокинутись', 'Попоїсти', 'Попрацювати'];

function runTask(taskName, callback) {
  console.log(`Старт. ${taskName}. ${new Date().getSeconds()}`);
  setTimeout(() => {
    console.log(`Фініш. ${taskName}. ${new Date().getSeconds()}`);
    callback(null);
  }, 1000);
}

runTask(tasks[0], err => {
  runTask(tasks[1], err => {
    runTask(tasks[2], err => {
      console.log('Кінець!');
    });
  });
});

1. Спочатку я створив список наших завдань
2. Функція runTask є саме тою асинхронною функцією, котра виконує якусь роботу, та закінчує виконання асинхронно. Старт сигналізує початок роботи, а фініш - кінець. Робота займає одну секунду.
3. Далі я викликає функцію передаючи в неї першу роботу з масиву робіт, при цьому передаю в нею колбек, котрий виконається лише після виконання першого виклику функції runTask. В цьому колбеці я викликаю ту саму функцію, але з іншим завданням, і так само ще раз.

Примітка: callback(null) та err всередині функцій додані спеціально, тому що за конценцією колбеків NodeJs першим аргументом колбеку повинна бути помилка, якщо така є. callback(null) як раз сигналізує, що помилки немає. Після неї я міг би передати ще якісь аргументи, котрі були б результатом виконання функції, але в цьому прикладі результату немає, тому я нічого не передаю.

Результатом виконання всього цього коду буде:

Старт. Прокинутись. 17
Фініш. Прокинутись. 18
Старт. Попоїсти. 18
Фініш. Попоїсти. 19
Старт. Попрацювати. 19
Фініш. Попрацювати. 20
Кінець!

Як видно з тексту та значення секунд - завдання виконується одне за одним, і виконання кожного завдання займає одну секунду.

Але уявімо, що у нас є дуже багато таких завдань, тоді такий код може швидко перерости в те, що в народі називається - пекло колбеків (callback hell), і виглядати воно може ось так.
https://content.altexsoft.com/media/2017/10/6cfYoU3.png

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

Подякували: Chemist-i1

17

Re: Патерни + Node.js

Як же ж можна вирішити цей код, аби воно не виглядало таким чином?

Ми можемо уявити виклик цих функцій як деяку повторювану операцію, а що у нас асоціюється з повторюванням? Або цикли, котрі виконують щось певну кількість разів, або ж рекурсія - коли функція викликає себе певну кількість разів.

const tasks = ['Прокинутись', 'Попоїсти', 'Попрацювати'];

function runTask(taskName, callback) {
  console.log(`Старт. ${taskName}. ${new Date().getSeconds()}`);
  setTimeout(() => {
    console.log(`Фініш. ${taskName}. ${new Date().getSeconds()}`);
    callback(null);
  }, 1000);
}

function runAllTasks(index) {
  if (index === tasks.length) return console.log('Кінець!');
  runTask(tasks[index], err => {
    runAllTasks(index + 1);
  });
}

runAllTasks(0);

Даний код працює наступним чином (я пропущу етап з масивом завдань).

1. Як бачите, основна функція, котра виконує корисні дії не змінились, вона ідентична тій, що ми бачили в попередньому коді.
2. А тут ми додаємо щось новеньке. Ми не викликаємо функцію runTask три рази вручну, а делегуємо цей функціонал іншій функції - runAllTasks.
Ця функція приймає індекс завдання, котре потрібно виконати, після чого перевіряє, чи ми бува, не виконали всіх завдань, і якщо ні, то відбувається наступне...
Ми викликаємо основну функцію, передаючи в неї колбек, в тілі котрого ми викликаємо runAllTasks знову, але вже з індексом завдання більшим на одиницю.
Таким чином після виконання основної функції, вона викличе цей колбек, котрий, своєю чергою ініціює виконання наступного завдання.

18

Re: Патерни + Node.js

Ну і останнім прикладом послідовного виконання асинхроних завдань буде приклад з асинхронними генераторами...

function runTask(taskName) {
  console.log(`Старт. ${taskName}. ${new Date().getSeconds()}`);
  return new Promise((resolve, reject) => {
    setTimeout(
      () => resolve(`Фініш. ${taskName}. ${new Date().getSeconds()}`),
      1000
    );
  });
}

async function* runAllTasks() {
  yield await runTask(tasks[0]);
  yield await runTask(tasks[1]);
  yield await runTask(tasks[2]);
  yield await Promise.resolve('Кінець!');
}

(async () => {
  for await (let _ of runAllTasks()) {
    console.log(_);
  }
})();
Старт. Прокинутись. 35
Фініш. Прокинутись. 36
Старт. Попоїсти. 36
Фініш. Попоїсти. 37
Старт. Попрацювати. 37
Фініш. Попрацювати. 38
Кінець!

Щоб пояснити, що тут відбувається детально, піде багато часу, бо сам концепт генераторів є досить цікавим, тому поясню коротко.

В циклі for, з функції-генератору runAllTasks ми отримуємо спеціальний об'єкт, котрий містить в собі функцію next. Ця функця повертає об'єкт у форматі

{value: якесь значення, done: true, або false}

value є тим значенням, котре ми повертаємо за допомогою ключового слова yield, ну а done просто сигналізує, чи цикл можна завершити.

Далі, сам цикл for викликає цю функцію next, та заносить значення, котре вона повертає, в змінну _ (я назвав її так, бо спершу не хотів її використовувати, а потім передумав, але змінювати її назву на щось нормально вже ліньки), після чого ми просто консоль.логаємо це значення.

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

Подякували: Chemist-i, leofun012

19

Re: Патерни + Node.js

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

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

const tasks = ['Прокинутись', 'Попоїсти', 'Попрацювати'].map(createTask);

function createTask(taskName) {
  return callback => {
    console.log(`Старт. ${taskName}. ${new Date().getSeconds()}`);
    setTimeout(() => {
      console.log(`Кінець. ${taskName}. ${new Date().getSeconds()}`);
      callback();
    }, 1000);
  };
}

function runTasks(tasks) {
  let done = 0;
  tasks.forEach(task => {
    task(() => {
      done++;
      if (done === tasks.length) {
        console.log(`Готово!`);
      }
    });
  });
}

runTasks(tasks);
Старт. Прокинутись. 49
Старт. Попоїсти. 49
Старт. Попрацювати. 49
Кінець. Прокинутись. 50
Кінець. Попоїсти. 50
Кінець. Попрацювати. 50
Готово!

Як бачите по результату - всі завдання почали працювати на 49-тій секунді, а закінчили на 50-тій.

20

Re: Патерни + Node.js

Тепер уявімо ситуацію, що нам необхідно обмежити кількість одночасно виконуваних завдань. Наприклад, ми хочемо, аби одночасно могло виконуватись не більше двох завдань, і як тільки якесь з цих двох завдань завершує роботу, то це означає, що "місце" звільняється, і ми можемо запустити ще одне завдання, котре може бути наступним в списку.

Це завдання можна вирішити змінивши попередній код, але я пропущу це, і перейду відразу до наступного прикладу, котрий використовуватиме "чергу" для реалізації поставленного завдання.

Черга - це черга.

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

Для реалізації такої черги пропоную створити окремий клас - TasksQueue.

class TasksQueue {
  tasks = []; // черга з завданнями (або клієнтами)
  running = 0; // кількість завдань, котрі виконуються в певний момент (або кількість клієнтів, що обслуговуються)

  constructor(concurrency) {
    this.concurrency = concurrency; // максимальна кількість завдань, котрі можуть виконуватись одночасно (або кількість співробітників в банку, що можуть обслуговувати клієнтів)
  }

  addTask(task) {
    this.tasks.unshift(task);
    this.doWork();
  }

  doWork() {
    while (this.tasks.length && this.running < this.concurrency) { // 1
      const task = this.tasks.pop(); // 2
      this.running++; // 3
      task(() => { // 4
        this.running--;
        this.doWork();
      });
    }
  }
}

Метод addTask дозволяє нам додати задачу до черги, а також намагається розпочати її виконання відразу після додавання.
doWork робить основну роботу.

1. В умові циклу ми перевіряємо, чи в черзі є якісь задачі, і чи поточна кількість задач, котрі виконуються, не більша за те число, котре ми передали до конструктора.
2. Якщо умова задовільнилась, то ми беремо з черги наступну задачу
3. Так, як ми збираємось виконати цю задачу, ми збільшуємо кількість задач, що виконуються, на одиницю.
4. І тут ми вже починаємо виконувати цю задачу, передаючи в неї колбек, котрий виконається тоді, коли задача буде виконаною. В цьому колбеці ми зменшуємо кількість задач, що виконуються, на одиницю, і знову запускаємо метод doWork аби той перевірим, чи бува в нас немає ще якихось задач для виконання.

Щоб перевірити роботу цього коду я додав трішки більше завдань

const tasks = [
  'Прокинутись',
  'Попоїсти',
  'Попрацювати',
  'Зайти на Replace',
  `Поставити вподобайку FakiNyan'у`,
  'Пожалітись на свою важку долю',
].map(createTask);

Тепер виконаємо цей код, і подивимось, що буде.

const tasksQueue = new TasksQueue(2); // максимальна кількість задач, що виконуються одночасно - 2 

tasks.forEach(task => tasksQueue.addTask(task));

Результатом виконання буде наступне...

Старт. Прокинутись. 49
Старт. Попоїсти. 49
Кінець. Прокинутись. 50
Старт. Попрацювати. 50
Кінець. Попоїсти. 50
Старт. Зайти на Replace. 50
Кінець. Попрацювати. 51
Старт. Поставити вподобайку FakiNyan'у. 51
Кінець. Зайти на Replace. 51
Старт. Пожалітись на свою важку долю. 51
Кінець. Поставити вподобайку FakiNyan'у. 52
Кінець. Пожалітись на свою важку долю. 52

Зверніть увагу на секунди. Кожне завдання повинно виконуватись одну секунду, але в нашому випадку ми можемо виконувати два завдання одночасно. Тому перші чотири рядки показують, що два завдання почали роботу на 49-тій секунді, а закінчили на 50-тій.

Всього у нас було 6 завдань, кожне з котрих виконувалось одну секунду, але так, як ми виконували кожні два завдання "паралельно" або "одночасно", то весь час на виконання 6-ьох завдань буде три секунди. І ми це бачимо, бо перше завдання почало роботу на 49-тій секунді, і останнє завдання закінчило роботу на 52-гій, тобто 52 - 39 = 3 секунди.

Якщо ми вкажемо 3 замість 2, тоді весь час буде всього дві секунди, ну а якщо вкажемо 1, тоді виконання шістьох завдань забере всі 6 секунд.

Подякували: leofun011