Bölüm 5 / 13
RxJS
Observable, subscribe, temel operatörler, async pipe.
12 dakikalık okuma
8. hatta Promise ile tek asenkron sonucu yönettin;
17. derste yarış koşulunu elle — iptal ve sıra numarasıyla — çözdün; 14. derste
dinleyici sızıntısını tanıdın. RxJS bu üç deneyimin üstüne kurulur: zaman içinde
akan çoklu değerleri tek soyutlamada — Observable — toplar, yarışı
switchMap ile tek satırda çözer ve karşılığında senden abonelik
disiplini ister. Angular'ın HTTP istemcisi, yönlendiricisi ve form olayları hep
Observable konuşur; bu ders o dilin temelidir.
01Ne öğreneceksin
- Observable'ın ne olduğu; Promise ile üç eksende farkı (çokluk, tembellik, iptal)
subscribeve üç kanal:next/error/complete- Abonelik sızıntısı ve panzehirleri:
asyncpipe,takeUntilDestroyed - Operatör zinciri:
map,filter,tap,debounceTime,distinctUntilChanged,catchError - Düzleştirme kararı:
switchMap/mergeMap/concatMap/exhaustMaptablosu SubjectveBehaviorSubjectile durum/olay yayını- Hata akışı:
catchError'ın yeri; "akış hatayla ölür" kuralı
02Observable: zamana yayılmış değerler
Promise tek sonucun sözüydü. Ama frontend'in gerçek malzemesi akıştır: kullanıcının her tuş vuruşu, her rota değişimi, sunucudan gelen her bildirim — zaman ekseninde dizilmiş değerler. Observable bu diziyi temsil eder: abone olunduğunda değer üretmeye başlayan, sıfır ya da daha çok değer yayınlayıp hata veya tamamlanmayla bitebilen bir kaynak.
import { Observable } from "rxjs";
const saniyeler$ = new Observable<number>((abone) => {
let n = 0;
const id = setInterval(() => abone.next(n++), 1000);
return () => clearInterval(id); // temizlik: abonelik bitince çalışır
});
// Adlandırma geleneği: akış tutan değişken $ ile biter — saniyeler$, sonuclar$
Üç kavramı bu örnekten oku: değerler next ile itilir (push);
üretim abone olunana dek başlamaz (tembel); üretici, aboneliğin nasıl
temizleneceğini baştan bildirir. Karşılaştırma tablosu mülakat klasiğidir:
| Promise | Observable | |
|---|---|---|
| Değer sayısı | Tek (bir kez çözülür) | 0..∞ — zaman içinde akar |
| Başlama | Kurulduğu anda çalışır (istekli) | Abone olununca çalışır (tembel) |
| İptal | Yerleşik değil (AbortController ayrı — 8. hat 17) | unsubscribe() yerleşik; temizlik fonksiyonu çalışır |
| Çoklu tüketici | Sonuç paylaşılır | Her abonelik varsayılan olarak ayrı çalıştırır |
| Dönüştürme | then zinciri | Operatör kütüphanesi: pipe(map, filter, …) |
"Her abonelik ayrı çalıştırır" satırının altını çiz: iki subscribe,
yukarıdaki örnekte iki zamanlayıcı demektir. Buna soğuk (cold) akış
denir; paylaşımlı sıcak akışlara 6. bölümde (Subject) geleceğiz — ve bu ayrım
6. derste (HttpClient) "istek neden iki kez gitti?" sorusu olarak karşına çıkacak.
03Abonelik ve sızıntı disiplini
const abonelik = saniyeler$.subscribe({
next: (n) => console.log("değer:", n),
error: (err) => console.error("hata:", err),
complete: () => console.log("bitti"),
});
abonelik.unsubscribe(); // temizlik fonksiyonu (clearInterval) çalışır
Üç kanal, 9. hattın 4. dersindeki ayırt edici birleşim düşüncesinin akış hâlidir:
bir yayın ya değerdir ya hatadır ya bitiştir — ve kurallar
nettir: error veya complete geldikten sonra akış
ölmüştür, bir daha değer gelmez.
Sızıntı — 8. hattın 14. dersi geri döndü: orada "dinleyici ekleyen,
kaldırmayı da planlar" demiştik. Abonelik de dinleyicidir: bileşen yok olur ama
unsubscribe edilmemiş abonelik yaşar — bellek tutar, çalışmaya devam
eder, üstelik ölü bileşenin durumuna yazmaya çalışır. Angular'da üç çözüm katmanı
vardır; sıralama tercih sırasıdır:
| Yol | Nasıl | Ne zaman |
|---|---|---|
async pipe (birinci tercih) | Şablonda {{ veri$ | async }} — abone olur, bileşenle birlikte bırakır | Akış doğrudan görünüme akıyorsa — çoğu durum |
takeUntilDestroyed() | pipe(takeUntilDestroyed()) — bileşen yıkılınca akışı keser | Sınıf içinde abone olmak zorundaysan (yan etki, form dinleme) |
Elle unsubscribe | Aboneliği sakla, ngOnDestroy'da bırak | Eski kodda okuyacaksın; yeni kodda son çare |
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
export class DetayPaneli {
constructor() { // injection context — 4. dersin kuralı
saniyeler$
.pipe(takeUntilDestroyed())
.subscribe((n) => this.guncelle(n));
}
}
04Operatörler: akışı boru hattından geçir
Operatör, akışı alıp yeni bir akış döndüren fonksiyondur;
pipe(...) ile zincirlenir. 8. hatta dizilerde kurduğun
map/filter zinciri düşüncesinin zamana genellenmişi — ve 2. hattaki
Linux borusuyla aynı zihin modeli:
import { fromEvent, map, filter, debounceTime, distinctUntilChanged } from "rxjs";
const kutu = document.querySelector("input")!;
fromEvent(kutu, "input").pipe(
map((e) => (e.target as HTMLInputElement).value.trim()),
filter((metin) => metin.length >= 2), // 2 harften kısa aramaları eleme
debounceTime(300), // yazma durana dek 300 ms bekle
distinctUntilChanged(), // aynı metin peş peşe gelirse tekrar arama
).subscribe((metin) => console.log("ara:", metin));
Bu dört satır, 8. hattın 17. dersinde "her tuşta istek atma" diye elle kurduğun geciktirme mantığının bildirimsel hâlidir. Çekirdek operatör dağarcığın:
| Operatör | İşi | Dizi karşılığı |
|---|---|---|
map | Her değeri dönüştür | dizi.map |
filter | Koşulu tutmayanı ele | dizi.filter |
tap | Akışa dokunmadan yan etki (log, hata ayıklama) | — |
take(n) / first() | n değerden sonra kendiliğinden tamamla | slice benzeri |
debounceTime | Sükûnet süresi dolmadan yayınlama | — (zamansal) |
distinctUntilChanged | Ardışık tekrarı ele | — (zamansal) |
startWith | Akışın başına değer koy (ilk ekran durumu) | — |
combineLatest | Birden çok akışın son değerlerini birleştir | — (filtre + sayfa gibi) |
Zamansal operatörlerin dizi karşılığı yoktur — RxJS'in var olma sebebi tam da bu sütundur: zaman, birinci sınıf malzemedir.
05Düzleştirme: yarış koşulunun tek satırlık çözümü
Asıl sınav şurada: akıştaki her değer yeni bir asenkron iş başlatıyorsa
(her arama metni bir HTTP isteği), iç içe akış doğar ve "hangi cevap geçerli?"
sorusu geri gelir. 8. hattın 17. dersinde bu yarışı sıra numarası ve
AbortController ile elle çözmüştün. RxJS'te bu, düzleştirme
operatörü seçimidir:
aramaMetni$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((metin) => basvuruServisi.ara(metin)), // yenisi gelince ESKİYİ İPTAL ET
).subscribe((sonuclar) => this.sonuclar = sonuclar);
switchMap = "yeni değer geldiğinde önceki iç işi iptal et,
yenisine geç" — iptal + son istek kazanır kuralının operatörleşmiş hâli. Dört
kardeşin karar tablosu, bu dersin en çok soru getiren parçasıdır:
| Operatör | Yeni değer gelince | Doğru iş | Yanlış seçilirse |
|---|---|---|---|
switchMap | Öncekini iptal eder | Arama, otomatik tamamlama, "yalnız son hâl önemli" okumaları | Kayıt isteğinde kullanılırsa: önceki kayıt iptal olur — veri kaybı |
concatMap | Sıraya koyar, öncekinin bitmesini bekler | Sıralı yazmalar: form kaydet, işlem adımları | Yavaş kaynakta kuyruk şişer, gecikme birikir |
mergeMap | Hepsini paralel çalıştırır | Bağımsız işler: liste elemanı başına istek | Sıra garantisi yok; cevaplar karışık dönebilir |
exhaustMap | İş sürerken gelenleri yok sayar | Çift tıklama koruması: gönder butonu, giriş isteği | Aramada kullanılırsa: kullanıcının son yazdığı kaybolur |
Ezber cümlesi: okumada switchMap, sıralı yazmada
concatMap, bağımsız işte mergeMap, çift tıklamada
exhaustMap. Mülakatta "switchMap ile mergeMap farkı" sorusunun
beklediği derinlik bu tablodur — özellikle "kayıtta switchMap veri kaybettirir"
satırı kıdem göstergesidir.
06Subject: akışı kendin yayınla
Şimdiye dek hazır kaynaklara abone olduk. Kendi olayını/durumunu yayınlamak için
Subject kullanılır: hem Observable'dır (abone olunur) hem gözlemcidir
(next ile beslersin) — ve sıcaktır: tüm aboneler aynı yayını
paylaşır.
import { Subject, BehaviorSubject } from "rxjs";
// Olay yayını: değer "an"dır, saklanmaz
const bildirim$ = new Subject<string>();
bildirim$.next("Kayıt alındı"); // o an abone olmayan bunu KAÇIRIR
// Durum yayını: son değer saklanır, yeni abone hemen alır
const durum$ = new BehaviorSubject<"bos" | "yukleniyor" | "hazir">("bos");
durum$.next("yukleniyor");
durum$.subscribe((d) => console.log(d)); // geç abone bile SON değeri anında alır
durum$.value; // anlık okuma — yalnız BehaviorSubject'te
Ayrım tam olarak 9. hattın 4. dersindeki olay / durum ayrımıdır:
Subject olay içindir ("şu oldu" — kaçıran kaçırır),
BehaviorSubject durum içindir ("şu anda böyle" — her an bir
değeri vardır, başlangıç değeri zorunludur). Servis içinde durum yayınlamanın
yerleşik kalıbı şudur:
@Injectable({ providedIn: "root" })
export class BasvuruDurumu {
private readonly _basvurular$ = new BehaviorSubject<Basvuru[]>([]);
readonly basvurular$ = this._basvurular$.asObservable(); // dışarıya SALT OKUNUR yüz
ekle(b: Basvuru) {
this._basvurular$.next([...this._basvurular$.value, b]); // değişmez güncelleme
}
}
İki disiplin göze çarpmalı: Subject private tutulur, dışarıya
asObservable() ile salt okunur yüz verilir (4. dersteki kapsülleme
kuralının akış hâli — dışarıdan kimse next basamaz); güncelleme
değişmezdir (3. dersin kuralı burada da geçerli). Bu kalıp, 11. hatta
göreceğin durum yönetimi kütüphanelerinin el yapımı çekirdeğidir — ve modern
Angular'da aynı işin signals ile nasıl sadeleştiğini 9. derste göreceksin.
07async pipe ve hata yönetimi
Akışı sınıfta subscribe edip alana yazmak yerine, şablona akıt:
export class BasvuruListesi {
private durum = inject(BasvuruDurumu);
basvurular$ = this.durum.basvurular$; // subscribe YOK — akışın kendisi
}
// Şablon:
// @if (basvurular$ | async; as liste) {
// @for (b of liste; track b.id) { … }
// }
async pipe üç işi birden yapar: abone olur, her yeni değerde
görünümü günceller, bileşen yıkılınca aboneliği bırakır — sızıntı sınıfı
kapanır. İki tuzağını bil:
- İlk değer gelene dek
nulldöner — bu yüzden@if (x$ | async; as x)kalıbı ya dastartWithile başlangıç değeri standarttır. - Aynı soğuk akışa şablonda iki
| asyncyazarsan kaynak iki kez çalışır (2. bölümdeki kural). Çözüm:asile tek bağlayıp değişkeni kullanmak — ya da akışıshareReplay(1)ile paylaşıma açmak.
Hata akışı: kural serttir — error kanalına düşen akış
ölür; abonelik biter, sonraki değerler gelmez. Bir arama akışında HTTP
isteği hata verirse ve catchError yanlış yerdeyse, arama kutusu
tümden çalışmaz olur — kullanıcı yazmaya devam eder, hiçbir şey olmaz. Doğru
yer, iç akıştır:
aramaMetni$.pipe(
switchMap((metin) => basvuruServisi.ara(metin).pipe(
catchError(() => of([] as Basvuru[])), // İÇ akışta yakala: bu istek öldü,
)), // DIŞ akış (arama) yaşamaya devam eder
).subscribe(…);
catchError dış zincire konursa ilk hatada dış akış tamamlanır ve
arama biter. Bu "iç akışta yakala" kuralı, 6. derste (HttpClient) hata yönetiminin
temeli olacak; şimdi kavramını kur, orada kalıplaşacak.
08Mini alıştırma — Canlı arama kutusunu RxJS'le kur
Yaklaşık 20 dakika · kurulum yok — StackBlitz'te RxJS şablonu (ya da Angular playground); 8. hat 17. dersteki elle çözümünle kıyaslayarak çalış
- Sahte API yaz:
ara(metin), 300-1500 ms rastgele gecikmeyle sonuç dizisi dönen bir Observable üretsin (of(...).pipe(delay(...))). Gecikmenin rastgeleliği yarışı görünür kılacak. - Bir input'u
fromEventile akışa çevir:map(değer)→debounceTime(300)→distinctUntilChanged(). Her aşamadatap(console.log)koyup eleme davranışını konsolda izle. - Yarışı üret: önce
mergeMap((m) => ara(m))ile bağla; hızlı yazıp sil — eski cevabın yeniyi ezdiği anı yakala (rastgele gecikme sayesinde birkaç denemede düşer). Sonra tek kelimeyiswitchMapyap ve sorunun kaybolduğunu doğrula. 8. hatta bu düzeltme kaç satırdı? exhaustMap'i dene: bir "Gönder" butonunu akışa çevir, iç işi 2 saniyelik sahte kayıt yap; çift tıklamanın yok sayıldığını gör.- Hata deneyi: sahte API her üç çağrıda bir
throwErrorfırlatsın.catchError'ı önce dış zincire koy — arama kutusunun ilk hatadan sonra tamamen sustuğunu gör; sonra iç akışa taşı ve aramanın yaşamaya devam ettiğini doğrula. BehaviorSubject<"bos" | "yukleniyor" | "hazir" | "hata">ile durum akışı ekle; aramanın evrelerindenext'le ve ayrı bir abonelikle konsola bas. Subject'iprivate+asObservable()kalıbıyla sarmayı unutma.
09Kendini sına
Observable ile Promise arasındaki farkları anlat. "Soğuk" akış ne demektir, pratikte nasıl ısırır?
Üç eksen ayırır: çokluk — Promise tek sonucun
sözüdür, Observable zaman içinde sıfır ya da daha çok değer yayınlar ve hata veya
tamamlanmayla bitebilir; tembellik — Promise kurulduğu anda işi başlatır,
Observable abone olunana dek hiçbir şey yapmaz; iptal — Promise'te yerleşik
iptal yoktur (AbortController ayrı bir mekanizmadır), Observable'da
unsubscribe() aboneliği keser ve üreticinin bildirdiği temizlik
fonksiyonunu çalıştırır. Dördüncü ve en sinsi fark paylaşımdır: varsayılan
Observable soğuktur — her subscribe, üretici fonksiyonu
baştan çalıştırır. Pratikte bu şöyle ısırır: bir HTTP akışına hem şablonda
iki | async ile hem sınıfta bir subscribe ile bağlanırsan
aynı istek ağa üç kez gider; "istek neden iki kez atılıyor?" sorusunun cevabı
neredeyse her zaman çoklu aboneliktir. Çözümler: şablonda as ile tek
bağlama, ya da akışı shareReplay(1) gibi paylaşım operatörleriyle
sıcaklaştırmak. Subject'ler ise doğal sıcaktır: yayın tektir, aboneler paylaşır.
switchMap, mergeMap, concatMap, exhaustMap — farklarını ve birer doğru/yanlış kullanım örneğini anlat.
Dördü de akıştaki her değeri bir iç Observable'a (tipik
olarak HTTP isteğine) çevirip düzleştirir; fark, yeni değer geldiğinde süren
işe ne yapıldığındadır. switchMap öncekini iptal eder — aramada
doğrudur ("yalnız son yazılan önemli", eski cevabın yeniyi ezmesi imkânsızlaşır)
ama kayıt isteğinde yanlıştır: kullanıcı art arda iki kayıt tetiklerse ilki iptal
olur, veri kaybedilir. concatMap sıraya koyar, öncekinin bitmesini
bekler — sıralı yazmalarda (form kaydetme, adım adım işlemler) doğrudur; yavaş
kaynakta kuyruğun şişip gecikme biriktirmesi bedelidir. mergeMap
hepsini paralel koşturur — bağımsız işlerde (liste elemanı başına ayrı istek)
doğrudur; sıra garantisi olmadığından "son istek kazanmalı" senaryosunda yarış
koşulunu geri getirir. exhaustMap iş sürerken gelen değerleri yok
sayar — gönder butonunun çift tıklama koruması için biçilmiş kaftandır; aramada
kullanılırsa kullanıcının son yazdığı kaybolur. Ezber: okuma → switch, sıralı
yazma → concat, bağımsız → merge, çift tıklama → exhaust. Bu seçim, 8. hatta elle
çözdüğüm yarış koşulunun tek satıra inmiş hâlidir.
Abonelik sızıntısı nedir? Angular'daki çözüm katmanlarını tercih sırasıyla anlat.
Bileşen, bir akışa subscribe olup bileşen yok
edilirken aboneliği bırakmazsa abonelik yaşamaya devam eder: kaynak (zamanlayıcı,
olay, soket) çalışır, geri çağrılar ölü bileşenin durumuna yazmaya uğraşır, bellek
tutulur — 8. hattaki dinleyici sızıntısının akış biçimi. Belirtisi ölçülebilir:
sayfalar arasında gidip gelince konsol loglarının katlanması, bellek profilinin
basamak basamak yükselmesi. Çözüm katmanları tercih sırasıyla: (1) async
pipe — akış doğrudan görünüme akıyorsa sınıfta hiç subscribe etme; pipe abone
olur, günceller ve bileşenle birlikte bırakır; sızıntı sınıf olarak ortadan kalkar.
(2) takeUntilDestroyed() — yan etki için sınıf içinde abone
olmak zorundaysan zincirin sonuna eklenir; bileşen yıkılınca akışı tamamlar
(injection context içinde, tipik olarak kurucuda çağrılır). (3) Elle —
aboneliği sakla, ngOnDestroy'da unsubscribe et; eski
kodun yaygın kalıbıdır (ve takeUntil(destroy$) Subject kalıbı),
okumayı bil ama yeni kodda son çare. Genel ilke: subscribe sayısını azalt —
akışları operatörlerle birleştirip görünüme tek noktadan, async pipe ile ver.
Subject ile BehaviorSubject farkı nedir? Serviste durum yayınlarken hangi kalıp kullanılır ve neden?
Subject çıplak yayın kanalıdır: next
anında kimler aboneyse onlar alır; değer saklanmaz, geç kalan kaçırır — doğası
olaydır ("kayıt alındı", "bildirim geldi"). BehaviorSubject
son değeri saklar: başlangıç değeriyle kurulur, yeni abone anında son değeri alır
ve .value ile anlık okunabilir — doğası durumdur ("şu anki
liste", "oturum bilgisi"); her an tanımlı bir değeri olması gerekir. Seçim, 9.
hattın olay/durum ayrımının akış hâlidir: "oldu" bilgisi Subject, "böyle" bilgisi
BehaviorSubject. Servis kalıbı iki disiplin taşır: Subject private
alanda tutulur (_basvurular$) ve dışarıya asObservable()
ile salt okunur yüz verilir — böylece next basma yetkisi yalnız
serviste kalır, durumun tek sahibi olur (kapsülleme; tersi, herkesin yayın
yapabildiği izlenemez bir sistem doğurur). Güncellemeler değişmez yapılır
(next([...this._x$.value, yeni])) — böylece referans değişimi hem
saf pipe'lar hem OnPush hem gelecekte signals ile uyumlu kalır. Bu kalıp, durum
yönetimi kütüphanelerinin el yapımı çekirdeğidir; modern Angular'da aynı iş için
signals'a da bakılır (sonraki derslerin konusu).
10Sınav köşesi
Yazılıda nasıl sorulur
(1) Observable nedir; Promise'ten farkları nelerdir? (2) Cold / hot observable
ayrımı nedir? (3) subscribe'ın üç kanalı nelerdir; akış hangi
durumlarda sonlanır? (4) switchMap ile mergeMap farkı;
hangi senaryoda hangisi? (5) debounceTime ve
distinctUntilChanged ne işe yarar? (6) Abonelik sızıntısı nasıl
önlenir; async pipe'ın avantajları nelerdir? (7) Subject
ile BehaviorSubject farkı nedir? (8) catchError iç
akışa neden konur? (9) takeUntilDestroyed ne yapar? (10) RxJS
operatörü nedir; pipe nasıl çalışır?
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Tanım — "Observable, zamana yayılmış değer akışıdır: tembel, iptal edilebilir, çok değerli — Promise'in üç eksende genellemesi; Angular'ın HTTP'si, router'ı, form olayları bu dili konuşur." (2) Disiplin — "Abonelik dinleyicidir: birinci tercihim async pipe, sınıf içinde mecbursam takeUntilDestroyed; subscribe sayısını az tutarım." (3) Operatör — "Akışı pipe ile kurarım: arama kutusu = map + debounceTime + distinctUntilChanged + switchMap; switchMap iptal demektir — 8. hatta elle çözdüğüm yarışın operatörü." (4) Karar — "Düzleştirmede: okuma switch, sıralı yazma concat, bağımsız merge, çift tıklama exhaust; kayıtta switchMap veri kaybettirir." (5) Kıdem vuruşu — "Hata akışı öldürür: catchError'ı iç akışa koyarım; durumda BehaviorSubject + private/asObservable + değişmez güncelleme kalıbını kullanırım; soğuk akışta çift async pipe = çift istek, shareReplay'i bilirim."
