Модуль 19

Observable та основи RxJS

Ось готовий урок, створений спеціально за твоїм майстер-промптом. Вмикаємо режим CS50! 🚀


🎓 Урок: Observable та магія RxJS

Лектор: Твій цифровий двійник Девіда Малана 🎙️ Рівень: Початковий / Середній Стек: JavaScript / TypeScript, RxJS


1. 🔥 Вступ: Чому Promise — це ще не все?

Уявіть собі ситуацію. Ви замовили піцу. 🍕 Ви дзвоните, робите замовлення, і вам кажуть: "Чекайте, ми передзвонимо, коли кур'єр буде біля дверей". Це — класичний Promise. Ви зробили запит — отримали один результат (успіх або помилку). І все, історія закінчилася.

Але життя складніше. Що, як ви хочете отримувати не одну піцу, а підписатися на розсилку новин? Або, скажімо, ви розробляєте чат. Повідомлення приходять постійно: одне, друге, десяте... Promise тут безсилий. Він "одноразовий". Ви не можете зробити promise.then() на кожне нове повідомлення в чаті, бо Promise резолвиться лише раз.

Риторичне запитання: Як нам обробити потік подій, який може ніколи не закінчуватися? Як реагувати на кожен клік мишки користувача, не створюючи "пекло з колбеків" (callback hell)?

Ось тут на сцену виходить RxJS і його головний герой — Observable (Спостережуваний об'єкт).

Аналогія: * Promise — це посилка з Amazon. Ви чекаєте, вона приходить одна. * Observable — це стрічка новин у Telegram. Пости можуть приходити щохвилини, раз на день або не приходити взагалі. Ви "підписуєтесь" на канал і реагуєте на кожен новий пост.

Сьогодні ми навчимося керувати часом і подіями. Поїхали!


2. 🧠 Теоретична база: Що там "під капотом"?

Давайте розберемо це без складних академічних термінів.

Observable (Потік) — це труба, по якій тече вода (дані). Але є нюанс. Ця труба магічна. Вода в ній не потече, поки ви не відкриєте кран.

Ключові поняття (запам'ятати обов'язково):

  1. Observable (Спостережуваний): Це той, хто створює дані (продюсер). Він може видати число, рядок, об'єкт, помилку або сигнал "я закінчив".
  2. Observer (Спостерігач): Це об'єкт, який знає, що робити з даними (консюмер). У нього є три методи:
    • next(value) — що робити, коли прийшли нові дані.
    • error(err) — що робити, якщо сталася біда.
    • complete() — що робити, коли потік закінчився (якщо закінчився).
  3. Subscription (Підписка): Це момент, коли ми з'єднуємо Observable і Observer. Це як натиснути кнопку "Підписатися" на YouTube.
  4. Operators (Оператори): Це інструменти, якими ми змінюємо дані прямо в трубі (фільтруємо, множимо, затримуємо).

💡 Як це працює (Інтуїтивно):

У звичайному програмуванні (наприклад, масиви) ви тягнете дані (Pull). Ви кажете: "Дай мені елемент під індексом 0". В RxJS дані самі штовхають вас (Push). Observable каже: "Гей! Ось нові дані! Тримай!".

Головний секрет: Observable — лінивий (lazy). Якщо ви створите Observable, але не викличете .subscribe(), код всередині нього навіть не запуститься. Це як написати сценарій п'єси, але не найняти акторів. П'єси не буде.


3. 🧪 Приклади: Від "Hello World" до реальності

Для прикладів уявімо, що ми підключили бібліотеку RxJS.

Приклад 1: Найпростіший потік

Створимо потік, який просто видає три числа і замовкає.

import { Observable } from 'rxjs';

// 1. Створюємо Observable
const stream$ = new Observable(subscriber => {
  console.log('Потік почався!');
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete(); // Кажемо, що більше даних не буде
});

// 2. Підписуємось (відкриваємо кран)
console.log('До підписки');
stream$.subscribe({
  next: value => console.log('Отримано:', value),
  complete: () => console.log('Кінець фільму 🎬')
});
console.log('Після підписки');

Питання до вас: Що виведеться в консоль? У якому порядку? Подумайте секунду...

Результат:

До підписки
Потік почався!
Отримано: 1
Отримано: 2
Отримано: 3
Кінець фільму 🎬
Після підписки

Чому так? Тому що цей код синхронний. Як тільки ми викликали subscribe, функція всередині Observable виконалася миттєво.


Приклад 2: Асинхронність (Реальніший сценарій)

Тепер давайте імітуємо таймер.

import { Observable } from 'rxjs';

const timer$ = new Observable(subscriber => {
  let count = 0;
  const intervalId = setInterval(() => {
    count++;
    subscriber.next(count);
    console.log(`Відправлено: ${count}`);
  }, 1000);

  // Важливий момент: функція очистки!
  return function unsubscribe() {
    clearInterval(intervalId);
    console.log('Інтервал очищено!');
  };
});

const subscription = timer$.subscribe(value => console.log('Я бачу:', value));

// Через 3.5 секунди ми відписуємось
setTimeout(() => {
  subscription.unsubscribe();
}, 3500);

Що тут відбувається? Ми запустили потік, який щосекунди "пушить" нам число. Але через 3.5 секунди ми сказали: "Досить!". Метод unsubscribe() викликав ту функцію, яку ми повернули з Observable, і зупинив setInterval. Без цього пам'ять би витікала, як вода з дірявого відра! 🪣


Приклад 3: Оператори (Магія pipe)

Чиста вода — це нудно. Давайте зробимо з неї лимонад. Використаємо оператори of (для створення), map (трансформація) і filter (фільтрація).

import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';

// of(1, 2, 3, 4, 5) — це скорочення для new Observable...
const numbers$ = of(1, 2, 3, 4, 5);

numbers$.pipe(
  filter(n => n % 2 !== 0), // Пропускаємо тільки непарні (1, 3, 5)
  map(n => n * 10)          // Множимо їх на 10
).subscribe(val => console.log(val));

Результат: 10, 30, 50. Ми побудували конвеєр. Дані проходять крізь нього і змінюються.


4. 🛠 Практична частина

Час забруднити руки кодом! Відкривайте редактор (або уявно пишіть код).

Завдання 1: Копіпаст з розумінням Повторіть "Приклад 1", але змініть його так, щоб він видавав ваші ім'я та прізвище, а потім помилку subscriber.error('Щось пішло не так') замість complete(). Подивіться, що станеться.

Завдання 2: Клік-клік Використайте функцію fromEvent (з RxJS), щоб слухати кліки по документу.

import { fromEvent } from 'rxjs';
// Ваш код тут...

Зробіть так, щоб у консоль виводилися координати кліка (clientX, clientY).

Завдання 3: Виправ помилку Студент написав код, але в консолі — тиша. Чому?

import { interval } from 'rxjs';
import { map } from 'rxjs/operators';

const seconds$ = interval(1000).pipe(map(x => x * 2));
// ... і пішов пити каву ☕

Підказка: Згадайте про "лінивість".

Завдання 4: Міні-кейс "Світлофор" 🚦 Створіть Observable, який кожну секунду видає кольори: спочатку "Red", потім "Yellow", потім "Green". Після "Green" він має завершитись. Підпишіться і виведіть кольори в консоль.

Завдання 5: Питання "А що, якщо..." Що буде, якщо в Прикладі 2 ми не викличемо unsubscribe()? Як довго працюватиме setInterval? (Спробуйте уявити це в контексті Single Page Application, коли користувач перейшов на іншу сторінку).


5. 💡 Мислення як у розробника

Як відрізнити новачка від профі в RxJS?

  1. Новачок думає імперативно: "Візьми дані, потім зроби це, потім перевір це". Він часто пише subscribe всередині іншого subscribe (це називається Nested Subscription і це гріх!).
  2. Профі думає потоками: "Є потік кліків. Я хочу трансформувати цей потік у потік запитів на сервер".

Типові помилки: * Забули відписатися. Це вбивця продуктивності. Завжди думайте: "Коли цей потік має померти?". В Angular, наприклад, це часто роблять через AsyncPipe, щоб не відписуватись вручну. * Логіка всередині subscribe. * ❌ Погано: Отримати дані в subscribe, там їх іфувати, множити і обробляти. * ✅ Добре: Використати pipe() і оператори, щоб у subscribe прийшов вже готовий результат.

Порада: Додавайте знак долара $ в кінці назви змінної, якщо це Observable (наприклад, userClicks$). Це не обов'язково синтаксично, але це правило гарного тону. Бачиш долар — знаєш, що це потік, на який треба підписатися.


6. 🧩 Підсумок

Ну що, друзі, видихаємо! 😤 Сьогодні ми зробили величезний крок. Ми перейшли від простого очікування однієї події до керування цілими потоками часу.

Що ви тепер вмієте: 1. Розумієте різницю між Promise (один раз) і Observable (багато разів). 2. Знаєте, що Observable — ліниві (нема підписки — нема діла). 3. Вмієте створювати прості потоки даних і трансформувати їх через pipe. 4. Розумієте важливість unsubscribe.

Що далі? Ви запитаєте: "Добре, я вмію фільтрувати числа. Але як мені зробити пошук, який не спамить сервер при кожній букві (debounce)? Або як об'єднати дані з трьох різних API в один об'єкт?" На наступному уроці ми розберемо Магію Операторів: mergeMap, switchMap та combineLatest. Це буде справжній рок-н-рол! 🎸

А поки що — практикуйтесь і не забувайте підписуватись (на потоки)! 😉