Skip to content

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)
  • subscribe ve üç kanal: next / error / complete
  • Abonelik sızıntısı ve panzehirleri: async pipe, takeUntilDestroyed
  • Operatör zinciri: map, filter, tap, debounceTime, distinctUntilChanged, catchError
  • Düzleştirme kararı: switchMap / mergeMap / concatMap / exhaustMap tablosu
  • Subject ve BehaviorSubject ile 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:

PromiseObservable
Değer sayısıTek (bir kez çözülür)0..∞ — zaman içinde akar
BaşlamaKurulduğu anda çalışır (istekli)Abone olununca çalışır (tembel)
İptalYerleşik değil (AbortController ayrı — 8. hat 17)unsubscribe() yerleşik; temizlik fonksiyonu çalışır
Çoklu tüketiciSonuç paylaşılırHer abonelik varsayılan olarak ayrı çalıştırır
Dönüştürmethen zinciriOperatö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:

YolNasılNe zaman
async pipe (birinci tercih)Şablonda {{ veri$ | async }} — abone olur, bileşenle birlikte bırakırAkış doğrudan görünüme akıyorsa — çoğu durum
takeUntilDestroyed()pipe(takeUntilDestroyed()) — bileşen yıkılınca akışı keserSınıf içinde abone olmak zorundaysan (yan etki, form dinleme)
Elle unsubscribeAboneliği sakla, ngOnDestroy'da bırakEski 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İşiDizi karşılığı
mapHer değeri dönüştürdizi.map
filterKoşulu tutmayanı eledizi.filter
tapAkışa dokunmadan yan etki (log, hata ayıklama)—
take(n) / first()n değerden sonra kendiliğinden tamamlaslice benzeri
debounceTimeSükûnet süresi dolmadan yayınlama— (zamansal)
distinctUntilChangedArdışık tekrarı ele— (zamansal)
startWithAkışın başına değer koy (ilk ekran durumu)—
combineLatestBirden ç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örYeni değer gelinceDoğru işYanlış seçilirse
switchMapÖncekini iptal ederArama, otomatik tamamlama, "yalnız son hâl önemli" okumalarıKayıt isteğinde kullanılırsa: önceki kayıt iptal olur — veri kaybı
concatMapSıraya koyar, öncekinin bitmesini beklerSıralı yazmalar: form kaydet, işlem adımlarıYavaş kaynakta kuyruk şişer, gecikme birikir
mergeMapHepsini paralel çalıştırırBağımsız işler: liste elemanı başına istekSıra garantisi yok; cevaplar karışık dönebilir
exhaustMapİş sürerken gelenleri yok sayarÇift tıklama koruması: gönder butonu, giriş isteğiAramada 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 null döner — bu yüzden @if (x$ | async; as x) kalıbı ya da startWith ile başlangıç değeri standarttır.
  • Aynı soğuk akışa şablonda iki | async yazarsan kaynak iki kez çalışır (2. bölümdeki kural). Çözüm: as ile 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ış

  1. 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.
  2. Bir input'u fromEvent ile akışa çevir: map(değer) → debounceTime(300) → distinctUntilChanged(). Her aşamada tap(console.log) koyup eleme davranışını konsolda izle.
  3. 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 kelimeyi switchMap yap ve sorunun kaybolduğunu doğrula. 8. hatta bu düzeltme kaç satırdı?
  4. 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.
  5. Hata deneyi: sahte API her üç çağrıda bir throwError fı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.
  6. BehaviorSubject<"bos" | "yukleniyor" | "hazir" | "hata"> ile durum akışı ekle; aramanın evrelerinde next'le ve ayrı bir abonelikle konsola bas. Subject'i private + 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."