RxJS Operatörlerini Anlamak: forkJoin, zip, combineLatest ve withLatestFrom
Giriş: Reaktif Programlama ve RxJS
Modern web uygulamaları, kullanıcı etkileşimleri, veri akışları ve zaman uyumsuz işlemlerle dolu karmaşık ortamlardır. Bu karmaşıklığı yönetmek için reaktif programlama paradigması ve özellikle RxJS (Reactive Extensions for JavaScript) gibi kütüphaneler geliştirilmiştir. RxJS, olayları, API çağrılarını ve diğer zaman uyumsuz veri kaynaklarını “Observable” adı verilen gözlemlenebilir akışlar olarak ele alarak, bu akışlar üzerinde güçlü ve deklaratif bir şekilde işlem yapmamızı sağlar.
Observable’lar, zamanla birden fazla değer yayan veya hiç değer yaymadan tamamlanabilen veya hata verebilen veri kaynaklarıdır. RxJS’in gücü, bu Observable’lar üzerinde işlem yapmamızı sağlayan zengin operatör setinden gelir. Bu operatörler, Observable’ları dönüştürmek, filtrelemek, birleştirmek veya yan etkiler oluşturmak için kullanılır.
Bu makalede, RxJS’in en sık kullanılan ve zaman zaman karıştırılabilen dört birleştirme operatörünü derinlemesine inceleyeceğiz: forkJoin, zip, combineLatest ve withLatestFrom. Her bir operatörün ne işe yaradığını, nasıl çalıştığını, hangi senaryolarda kullanılması gerektiğini, örnek kodlarla ve karşılaştırmalı analizlerle açıklayacağız. Bu sayede, uygulamanızın ihtiyaçlarına en uygun operatörü seçme konusunda daha bilinçli kararlar verebileceksiniz.
RxJS Operatörleri: Birleştirme ve Senkronizasyon
Çoğu zaman, uygulamalarımızda birden fazla zaman uyumsuz veri kaynağını aynı anda veya belirli bir mantık dahilinde birleştirmemiz gerekir. Örneğin, bir kullanıcının profilini göstermek için hem kullanıcı bilgilerini hem de son siparişlerini getiren iki farklı API çağrısını beklememiz gerekebilir. Veya bir formdaki birden fazla giriş alanının değerlerini aynı anda izleyerek bir butonu etkinleştirmemiz gerekebilir. İşte bu gibi senaryolarda forkJoin, zip, combineLatest ve withLatestFrom gibi birleştirme operatörleri devreye girer. Her birinin farklı bir birleştirme stratejisi ve kullanım amacı vardır.
forkJoin: Tüm Akışların Tamamlanmasını Beklemek
Nedir ve Nasıl Çalışır?
forkJoin, bir veya daha fazla Observable’ı girdi olarak alan bir RxJS operatörüdür. Temel prensibi şudur: kendisine verilen tüm Observable’ların tamamlanmasını bekler. Tüm Observable’lar başarıyla tamamlandığında, her bir Observable’dan yayılan son değerleri bir dizi veya bir nesne olarak birleştirir ve tek bir değer olarak yayar. Bu operatör, Promise.all() fonksiyonunun Observable karşılığı olarak düşünülebilir.
Eğer girdi olarak verilen Observable’lardan herhangi biri tamamlanmadan önce bir hata yayarsa, forkJoin anında bu hatayı yayar ve diğer Observable’ları iptal eder. Ayrıca, eğer bir Observable hiç değer yaymadan tamamlanırsa, forkJoin bu Observable için tanımsız (undefined) bir değer döndürecektir. Bu nedenle, forkJoin kullanırken tüm Observable’ların en az bir değer yaydığından ve sonunda tamamlandığından emin olmak önemlidir.
Kullanım Senaryoları
forkJoin genellikle şu senaryolarda tercih edilir:
* Paralel API Çağrıları: Bir sayfayı yüklemek için birden fazla bağımsız API çağrısı yapmanız ve tüm verilerin gelmesini bekleyip tek bir görünümde birleştirmeniz gerektiğinde. Örneğin, bir ürün detay sayfasında ürün bilgisi, stok durumu ve müşteri yorumları gibi farklı verileri aynı anda çekmek.
* Sabit Veri Setleri: Birkaç bağımsız ve tamamlanacak veri kaynağını birleştirmeniz gerektiğinde. Örneğin, bir rapor oluşturmak için farklı veri kümelerini aynı anda yüklemek.
* Başlangıç Verileri: Uygulama başlangıcında veya belirli bir bileşenin yüklenmesinde gerekli olan tüm başlangıç verilerini toplamak.
Örnek Kullanım
import { forkJoin, of, timer, throwError } from 'rxjs';
import { delay, take } from 'rxjs/operators';
console.log('forkJoin Örneği Başladı...');
// 1. Basit Başarılı Senaryo
const kaynak1 = of('A').pipe(delay(1000)); // 1 saniye sonra 'A' yayar ve tamamlanır
const kaynak2 = of('B').pipe(delay(2000)); // 2 saniye sonra 'B' yayar ve tamamlanır
const kaynak3 = of('C').pipe(delay(500)); // 0.5 saniye sonra 'C' yayar ve tamamlanır
forkJoin([kaynak1, kaynak2, kaynak3]).subscribe(
degerler => console.log('Başarılı forkJoin (Dizi):', degerler), // ['A', 'B', 'C']
hata => console.error('Hata:', hata),
() => console.log('Başarılı forkJoin tamamlandı.')
);
// Çıktı (2 saniye sonra):
// Başarılı forkJoin (Dizi): [ 'A', 'B', 'C' ]
// Başarılı forkJoin tamamlandı.
// 2. Nesne Destructuring ile Kullanım
const kullaniciApi = of({ id: 1, ad: 'Ali' }).pipe(delay(1500));
const siparisApi = of([{ urun: 'Kitap' }, { urun: 'Kalem' }]).pipe(delay(1000));
forkJoin({
kullanici: kullaniciApi,
siparisler: siparisApi
}).subscribe(
veri => console.log('Başarılı forkJoin (Nesne):', veri),
hata => console.error('Hata:', hata),
() => console.log('Nesne forkJoin tamamlandı.')
);
// Çıktı (1.5 saniye sonra):
// Başarılı forkJoin (Nesne): { kullanici: { id: 1, ad: 'Ali' }, siparisler: [ { urun: 'Kitap' }, { urun: 'Kalem' } ] }
// Nesne forkJoin tamamlandı.
// 3. Hata Senaryosu
const hataKaynak = throwError(() => new Error('API Hatası')).pipe(delay(2500));
const kaynak4 = of('D').pipe(delay(500));
forkJoin([kaynak4, hataKaynak]).subscribe(
degerler => console.log('Hatalı forkJoin (Dizi):', degerler),
hata => console.error('Hata Senaryosu:', hata.message), // API Hatası
() => console.log('Hatalı forkJoin tamamlandı.') // Bu mesaj çalışmaz
);
// Çıktı (2.5 saniye sonra):
// Hata Senaryosu: API Hatası
// 4. Uzun Süre Yayan Observable ile Senaryo (take operatörü olmadan asla tamamlanmaz)
// const uzunYayanKaynak = timer(0, 1000); // Her saniye bir değer yayar, asla tamamlanmaz
// forkJoin([kaynak1, uzunYayanKaynak]).subscribe(
// degerler => console.log('Uzun Yayan forkJoin:', degerler),
// hata => console.error('Hata:', hata),
// () => console.log('Uzun Yayan forkJoin tamamlandı.')
// );
// Yukarıdaki kod hiçbir zaman çıktı vermez, çünkü uzunYayanKaynak asla tamamlanmaz.
// forkJoin'in çalışması için tüm kaynakların tamamlanması gerekir.
// Eğer uzunYayanKaynak.pipe(take(1)) gibi bir operatör eklenirse çalışır.
Dikkat Edilmesi Gerekenler
* Tamamlanma Şartı: forkJoin‘in değer yayabilmesi için tüm girdi Observable’larının tamamlanması zorunludur. Eğer bir Observable asla tamamlanmazsa (örneğin, interval veya fromEvent gibi operatörlerle oluşturulan Observable’lar take veya takeUntil gibi bir tamamlama operatörü olmadan), forkJoin da asla değer yaymaz veya tamamlanmaz.
* Hata Yönetimi: Herhangi bir Observable hata yayarsa, forkJoin hemen bu hatayı yayar ve aboneliği sonlandırır. Bu, hataları merkezi bir yerde yönetmek için kullanışlı olabilir, ancak bazen kısmi başarıları ele almak isteyebilirsiniz. Bu durumda, catchError gibi operatörlerle her bir Observable’ı ayrı ayrı ele almanız gerekebilir.
Son Değer: Yalnızca her bir Observable’dan yayılan son* değerler birleştirilir. Ara değerler göz ardı edilir.
zip: Değerleri Sırayla Eşleştirmek
Nedir ve Nasıl Çalışır?
zip, kendisine verilen Observable’lardan gelen değerleri, yayılma sırasına göre eşleştiren bir birleştirme operatörüdür. zip operatörü, her bir Observable’dan birer değer aldığında, bu değerleri bir dizi veya belirtilen bir projeksiyon fonksiyonu aracılığıyla birleştirir ve yeni bir değer olarak yayar. Bir sonraki birleştirmeyi yapabilmek için, her bir Observable’dan tekrar birer değer gelmesini bekler.
Bu operatör, Observable’lar arasında bir “senkronizasyon” veya “eşleştirme” mekanizması sağlar. Bir Observable diğerlerinden daha hızlı değer yayarsa, zip diğer Observable’lardan değer gelmesini bekleyene kadar iç tamponunda bu fazla değerleri tutar. Eğer bir Observable diğerlerinden daha az değer yayar ve tamamlanırsa, zip işlemi de o Observable’ın tamamlanmasıyla birlikte sona erer. Yani, en kısa Observable’ın uzunluğu, zip operatörünün kaç kez değer yayacağını belirler.
Kullanım Senaryoları
zip genellikle şu senaryolarda kullanılır:
* Eşleşen Veri Akışları: Birden fazla bağımsız veri akışının değerlerini sırayla birleştirmeniz gerektiğinde. Örneğin, bir kullanıcının tıklama olayları ile her tıklamada çekilen verileri eşleştirmek.
* Form Girişleri: Birden fazla form alanının değerlerini aynı anda izleyerek, her bir alanın eşleşen değerlerini birleştirmek (ancak combineLatest daha sık kullanılır).
* Sıralı İşlemler: Bir dizi işlemi belirli bir sıraya göre ve her adımda farklı bir Observable’dan veri alarak yapmak.
* Çoklu Sayfalandırma: Bir API’den farklı sayfaları paralel olarak çekip, her sayfanın verisini sırasıyla işlemek.
Örnek Kullanım
import { zip, of, interval, timer } from 'rxjs';
import { take, map } from 'rxjs/operators';
console.log('zip Örneği Başladı...');
// 1. Basit Senaryo
const sayilar = of(1, 2, 3);
const harfler = of('A', 'B', 'C', 'D'); // Harfler Observable'ı daha uzun
zip(sayilar, harfler).subscribe(
degerler => console.log('Basit zip:', degerler), // [1, 'A'], [2, 'B'], [3, 'C']
hata => console.error('Hata:', hata),
() => console.log('Basit zip tamamlandı.')
);
// Çıktı:
// Basit zip: [ 1, 'A' ]
// Basit zip: [ 2, 'B' ]
// Basit zip: [ 3, 'C' ]
// Basit zip tamamlandı.
// (D harfi eşleşecek bir sayı bulamadığı için göz ardı edilir)
// 2. Zaman Farklılıkları ile Senaryo
const saniye = interval(1000).pipe(take(3)); // 0, 1, 2 değerlerini 1er saniye arayla yayar
const tiklama = timer(500, 2000).pipe(take(2), map(val => Click ${val + 1})); // 0.5s sonra Click 1, 2.5s sonra Click 2 yayar
zip(saniye, tiklama).subscribe(
degerler => console.log('Zaman Farklılıkları ile zip:', degerler),
hata => console.error('Hata:', hata),
() => console.log('Zaman Farklılıkları ile zip tamamlandı.')
);
// Çıktı:
// (1 saniye sonra)
// saniye: 0
// (0.5 saniye sonra)
// tiklama: Click 1
// (zip ilk eşleşmeyi yapar)
// Zaman Farklılıkları ile zip: [ 0, 'Click 1' ]
// (1.5 saniye sonra)
// saniye: 1
// (0.5 saniye sonra)
// tiklama: Click 2
// (zip ikinci eşleşmeyi yapar)
// Zaman Farklılıkları ile zip: [ 1, 'Click 2' ]
// (1 saniye sonra)
// saniye: 2
// (tiklama Observable'ı tamamlandığı için zip tamamlanır, saniye: 2 eşleşemez)
// Zaman Farklılıkları ile zip tamamlandı.
// 3. Projeksiyon Fonksiyonu ile Kullanım
const isimler = of('Ayşe', 'Fatma', 'Mehmet');
const yaslar = of(25, 30, 35);
zip(isimler, yaslar, (isim, yas) => ({ isim, yas }))
.subscribe(
kisi => console.log('Projeksiyon ile zip:', kisi),
hata => console.error('Hata:', hata),
() => console.log('Projeksiyon ile zip tamamlandı.')
);
// Çıktı:
// Projeksiyon ile zip: { isim: 'Ayşe', yas: 25 }
// Projeksiyon ile zip: { isim: 'Fatma', yas: 30 }
// Projeksiyon ile zip: { isim: 'Mehmet', yas: 35 }
// Projeksiyon ile zip tamamlandı.
Dikkat Edilmesi Gerekenler
* Eşleşme Kuralı: zip, her bir Observable’dan sırayla birer değer alana kadar bekler. Bu, tüm Observable’ların bir değer yaydığından emin olmak için bir tamponlama mekanizması kullanır.
* En Kısa Akış: zip operatörünün tamamlanması, kendisine verilen en kısa Observable’ın tamamlanmasıyla gerçekleşir. Daha uzun Observable’lardan gelen fazla değerler göz ardı edilir.
* Zamanlama: Observable’ların değer yayma hızları farklı olsa bile, zip değerleri sırasına göre eşleştirecektir. Bu, zamanlamadan ziyade sıraya odaklanılan durumlar için idealdir.
* Hata Yönetimi: forkJoin gibi, zip de herhangi bir Observable’dan gelen bir hatayı hemen yayar ve tüm abonelikleri sonlandırır.
combineLatest: En Son Değerleri Birleştirmek
Nedir ve Nasıl Çalışır?
combineLatest, kendisine verilen Observable’lardan herhangi biri bir değer yaydığında, diğer tüm Observable’lardan yayılan en son değerleri birleştirerek yeni bir değer yayan bir operatördür. Bu, dinamik ve sürekli güncellenen durumları izlemek için çok kullanışlıdır.
combineLatest‘in temel özelliği şudur:
1. İlk Emisyon: Bir değer yayabilmesi için, kendisine verilen tüm Observable’ların en az birer değer yaymış olması gerekir. Bu “ilk emisyon” koşulu sağlandıktan sonra, herhangi bir Observable yeni bir değer yaydığında, combineLatest hemen diğer tüm Observable’lardan o anki en son değerleri alarak yeni bir birleştirilmiş değer yayar.
2. Sürekli Güncelleme: Herhangi bir Observable yeni bir değer yaydığında, çıktı Observable’ı da yeni bir değer yayar. Bu, sürekli güncellenen birleşik bir duruma sahip olmanızı sağlar.
combineLatest, zip‘ten farklı olarak değerleri sırayla eşleştirmez; her zaman her bir Observable’dan gelen en son değeri kullanır. forkJoin‘den farklı olarak ise Observable’ların tamamlanmasını beklemez, sürekli olarak değer yaymaya devam edebilir (Observable’lar tamamlanana kadar veya abonelik sonlanana kadar).
Kullanım Senaryoları
combineLatest genellikle şu senaryolarda tercih edilir:
* Dinamik UI Durumları: Birden fazla form giriş alanının (örneğin, kullanıcı adı, şifre, e-posta) değerlerini izleyerek bir “Gönder” butonunun etkinliğini yönetmek. Herhangi bir giriş değiştiğinde, diğerlerinin en son değerleriyle birlikte yeni bir durum elde edilir.
* Filtreleme ve Sıralama: Bir ürün listesinin filtreleme seçenekleri (kategori, fiyat aralığı) ve sıralama seçenekleri (artan/azalan) gibi birden fazla parametrenin birleşimiyle güncellenen bir veri akışı oluşturmak. Herhangi bir filtre veya sıralama değiştiğinde, liste yeniden hesaplanır.
* Gerçek Zamanlı Gösterge Tabloları: Farklı sensörlerden veya veri kaynaklarından gelen verileri birleştirerek dinamik bir gösterge tablosu oluşturmak.
* Hesaplanan Değerler: Birkaç bağımsız Observable’dan gelen değerlere dayanarak sürekli olarak yeni bir değer hesaplamak.
Örnek Kullanım
import { combineLatest, of, interval, timer } from 'rxjs';
import { delay, take, startWith, map } from 'rxjs/operators';
console.log('combineLatest Örneği Başladı...');
// 1. Basit Senaryo
const kaynakA = of('A1', 'A2').pipe(delay(1000)); // 1s sonra A1, sonra A2 yayar
const kaynakB = of('B1').pipe(delay(500)); // 0.5s sonra B1 yayar
combineLatest([kaynakA, kaynakB]).subscribe(
degerler => console.log('Basit combineLatest:', degerler),
hata => console.error('Hata:', hata),
() => console.log('Basit combineLatest tamamlandı.')
);
// Çıktı:
// (0.5 saniye sonra)
// kaynakB: B1 yayar. kaynakA henüz değer yaymadığı için combineLatest beklemede.
// (0.5 saniye daha sonra, toplam 1 saniye)
// kaynakA: A1 yayar. Artık her iki Observable da bir değer yaydığı için combineLatest ilk değerini yayar.
// Basit combineLatest: [ 'A1', 'B1' ]
// (kaynakA'dan A2 gelir, kaynakB'nin son değeri hala B1)
// Basit combineLatest: [ 'A2', 'B1' ]
// (Her iki Observable da tamamlandığı için combineLatest de tamamlanır.)
// Basit combineLatest tamamlandı.
// 2. Form Girişleri Benzeri Senaryo
const kullaniciAdi$ = timer(0, 2000).pipe(
map(i => Kullanıcı_${i}),
take(3),
startWith(''), // Başlangıç değeri ile combineLatest'in hemen başlamasını sağlar
delay(100) // Küçük bir gecikme ekleyelim
);
const sifre$ = timer(500, 2000).pipe(
map(i => Sifre_${i}),
take(2),
startWith(''), // Başlangıç değeri ile combineLatest'in hemen başlamasını sağlar
delay(50) // Küçük bir gecikme ekleyelim
);
combineLatest([kullaniciAdi$, sifre$]).subscribe(
([kullaniciAdi, sifre]) => {
const gecerli = kullaniciAdi.length > 0 && sifre.length > 0;
console.log(Kullanıcı Adı: ${kullaniciAdi}, Şifre: ${sifre}, Geçerli: ${gecerli});
},
hata => console.error('Hata:', hata),
() => console.log('Form combineLatest tamamlandı.')
);
// Çıktı:
// (Hemen, startWith sayesinde)
// Kullanıcı Adı: , Şifre: , Geçerli: false
// (0.1 saniye sonra, kullaniciAdi$ ilk değerini yayar)
// Kullanıcı Adı: Kullanıcı_0, Şifre: , Geçerli: false
// (0.5 saniye sonra, sifre$ ilk değerini yayar)
// Kullanıcı Adı: Kullanıcı_0, Şifre: Sifre_0, Geçerli: true
// (2 saniye sonra, kullaniciAdi$ ikinci değerini yayar)
// Kullanıcı Adı: Kullanıcı_1, Şifre: Sifre_0, Geçerli: true
// (2 saniye sonra, sifre$ ikinci değerini yayar)
// Kullanıcı Adı: Kullanıcı_1, Şifre: Sifre_1, Geçerli: true
// (2 saniye sonra, kullaniciAdi$ üçüncü değerini yayar)
// Kullanıcı Adı: Kullanıcı_2, Şifre: Sifre_1, Geçerli: true
// Form combineLatest tamamlandı.
Dikkat Edilmesi Gerekenler
* İlk Emisyon Koşulu: combineLatest‘in değer yaymaya başlaması için tüm girdi Observable’larının en az birer değer yaymış olması gerekir. Eğer bir Observable başlangıçta hiç değer yaymazsa, startWith operatörü ile bir başlangıç değeri vererek bu durumu aşabilirsiniz.
* Her Değişiklikte Tetiklenme: Herhangi bir girdi Observable’ı yeni bir değer yaydığında, combineLatest hemen yeni bir birleştirilmiş değer yayar. Bu, sürekli güncellenen birleşik durumlar için çok güçlüdür.
* Hata Yönetimi: forkJoin ve zip gibi, combineLatest de herhangi bir girdi Observable’ından gelen bir hatayı hemen yayar ve tüm abonelikleri sonlandırır.
* Tamamlanma: combineLatest, tüm girdi Observable’ları tamamlandığında tamamlanır. Eğer bir Observable asla tamamlanmazsa, combineLatest de asla tamamlanmaz.
withLatestFrom: Kaynak Akışı Tetiklediğinde Diğerlerinden En Son Değeri Almak
Nedir ve Nasıl Çalışır?
withLatestFrom, diğerlerinden farklı olarak bir instance operatörüdür, yani bir Observable’ın pipe metodu içinde kullanılır. Bu operatör, bir “kaynak” Observable’dan (üzerinde withLatestFrom uygulanan Observable) bir değer yayıldığında tetiklenir. Tetiklendiğinde, withLatestFrom operatörüne verilen “diğer” Observable’lardan o anki en son değerleri alır ve kaynak Observable’dan gelen değerle birlikte bir dizi olarak birleştirip yayar.
combineLatest‘ten temel farkı şudur: combineLatest herhangi bir Observable değer yaydığında tetiklenirken, withLatestFrom sadece kaynak Observable değer yaydığında tetiklenir. Diğer Observable’lar kendi başlarına değer yaydıklarında, withLatestFrom bir çıktı yaymaz; sadece en son değerlerini dahili olarak günceller ve kaynak Observable’ın bir sonraki yaymasını bekler.
Kullanım Senaryoları
withLatestFrom genellikle şu senaryolarda tercih edilir:
* Kullanıcı Eylemlerini Zenginleştirme: Bir kullanıcının bir butona tıklama olayı (kaynak Observable) ile o anki form verilerini veya seçili filtreleri (diğer Observable’lar) birleştirmek. Kullanıcı tıkladığında, o anki bağlam verileriyle birlikte tıklama olayı işlenir.
* Bağlama Duyarlı İşlemler: Bir Observable’dan gelen veriyi, başka bir Observable’dan gelen ve daha yavaş değişen veya daha az sıklıkla yayılan bir “bağlam” veya “yapılandırma” verisiyle birleştirmek.
* Durum Bağımlı Eylemler: Bir olayın (kaynak) gerçekleştiği anda, uygulamanın o anki durumunu (diğer Observable) yakalamak.
Örnek Kullanım
import { of, interval, timer } from 'rxjs';
import { withLatestFrom, take, map, startWith } from 'rxjs/operators';
console.log('withLatestFrom Örneği Başladı...');
// Kaynak Observable: Kullanıcı tıklamaları gibi daha sık veya belirli olaylar
const tiklama$ = timer(0, 2000).pipe(
map(i => Tıklama_${i + 1}),
take(3)
);
// Diğer Observable: Kullanıcı ayarları, seçili tema gibi daha yavaş değişen durumlar
const ayarlar$ = timer(0, 1000).pipe(
map(i => Ayarlar_V${i + 1}),
take(5),
startWith('Ayarlar_Varsayılan') // Başlangıç değeri ile withLatestFrom'un hemen çalışabilmesini sağlar
);
tiklama$.pipe(
withLatestFrom(ayarlar$)
).subscribe(
([tiklama, ayarlar]) => console.log(Tıklama: ${tiklama}, Güncel Ayarlar: ${ayarlar}),
hata => console.error('Hata:', hata),
() => console.log('withLatestFrom tamamlandı.')
);
// Çıktı:
// (0 saniye)
// ayarlar$: Ayarlar_Varsayılan yayar.
// tiklama$: Tıklama_1 yayar. (Kaynak tetiklendi, ayarlar$'dan en son değeri alır.)
// Tıklama: Tıklama_1, Güncel Ayarlar: Ayarlar_Varsayılan
// (1 saniye)
// ayarlar$: Ayarlar_V1 yayar. (withLatestFrom bir şey yaymaz, sadece iç durumunu günceller.)
// (2 saniye)
// tiklama$: Tıklama_2 yayar. (Kaynak tetiklendi, ayarlar$'dan en son değeri alır.)
// Tıklama: Tıklama_2, Güncel Ayarlar: Ayarlar_V1
// ayarlar$: Ayarlar_V2 yayar. (withLatestFrom bir şey yaymaz.)
// (3 saniye)
// ayarlar$: Ayarlar_V3 yayar. (withLatestFrom bir şey yaymaz.)
// (4 saniye)
// tiklama$: Tıklama_3 yayar. (Kaynak tetiklendi, ayarlar$'dan en son değeri alır.)
// Tıklama: Tıklama_3, Güncel Ayarlar: Ayarlar_V3
// ayarlar$: Ayarlar_V4 yayar. (withLatestFrom bir şey yaymaz.)
// (5 saniye)
// ayarlar$: Ayarlar_V5 yayar ve tamamlanır. (withLatestFrom bir şey yaymaz.)
// withLatestFrom tamamlandı. (Kaynak Observable da tamamlandığı için.)
// 2. Projeksiyon Fonksiyonu ile Kullanım
const sepetUrunSayisi$ = interval(3000).pipe(
map(i => i + 1),
take(3),
startWith(0)
); // 0, 1, 2, 3
const kullaniciDurumu$ = interval(1000).pipe(
map(i => (i % 2 === 0 ? 'Çevrimiçi' : 'Çevrimdışı')),
take(7),
startWith('Bilinmiyor')
);
sepetUrunSayisi$.pipe(
withLatestFrom(kullaniciDurumu$, (urunSayisi, durum) => ({ urunSayisi, durum }))
).subscribe(
veri => console.log('Sepet ve Kullanıcı Durumu:', veri),
hata => console.error('Hata:', hata),
() => console.log('Sepet ve Kullanıcı Durumu tamamlandı.')
);
// Çıktı:
// (Hemen, sepetUrunSayisi$ ilk değerini yayar)
// Sepet ve Kullanıcı Durumu: { urunSayisi: 0, durum: 'Bilinmiyor' }
// (1s) kullaniciDurumu$: 'Çevrimiçi'
// (2s) kullaniciDurumu$: 'Çevrimdışı'
// (3s) sepetUrunSayisi$: 1 yayar.
// Sepet ve Kullanıcı Durumu: { urunSayisi: 1, durum: 'Çevrimdışı' }
// (3s) kullaniciDurumu$: 'Çevrimiçi'
// (4s) kullaniciDurumu$: 'Çevrimdışı'
// (5s) kullaniciDurumu$: 'Çevrimiçi'
// (6s) sepetUrunSayisi$: 2 yayar.
// Sepet ve Kullanıcı Durumu: { urunSayisi: 2, durum: 'Çevrimiçi' }
// (6s) kullaniciDurumu$: 'Çevrimdışı'
// (7s) kullaniciDurumu$: 'Çevrimiçi'
// Sepet ve Kullanıcı Durumu tamamlandı.
Dikkat Edilmesi Gerekenler
İlk Emisyon Koşulu: withLatestFrom‘un değer yayabilmesi için, kaynak Observable’ın bir değer yayması ve aynı zamanda kendisine verilen tüm diğer Observable’ların da en az birer değer yaymış olması* gerekir. Bu nedenle, startWith operatörü diğer Observable’lar için sıklıkla kullanılır.
* Tetikleyici: Yalnızca kaynak Observable yeni bir değer yaydığında çıktı Observable’ı bir değer yayar. Diğer Observable’lar kendi başlarına değer yaydıklarında bir çıktı oluşmaz.
* Hata Yönetimi: Kaynak Observable veya herhangi bir diğer Observable bir hata yayarsa, withLatestFrom hemen bu hatayı yayar ve tüm abonelikleri sonlandırır.
* Tamamlanma: Kaynak Observable tamamlandığında, withLatestFrom da tamamlanır. Diğer Observable’ların tamamlanması, kaynak Observable hala değer yayıyorsa withLatestFrom‘un tamamlanmasına neden olmaz, ancak o Observable’dan artık yeni en son değerler alınamaz.
Operatörlerin Karşılaştırılması ve Doğru Seçim
Bu dört operatörün her biri, birden fazla Observable’ı birleştirme amacına hizmet eder, ancak bunu farklı tetikleyiciler ve stratejilerle yaparlar. Doğru operatörü seçmek, uygulamanızın mantığını doğru bir şekilde ifade etmek ve beklenmedik davranışlardan kaçınmak için kritik öneme sahiptir.
Aşağıdaki tablo, bu operatörlerin temel özelliklerini özetlemektedir:
| Özellik | forkJoin | zip | combineLatest | withLatestFrom |
| :—————- | :———————————————- | :————————————————— | :—————————————————– | :———————————————– |
| Tetikleyici | Tüm kaynaklar tamamlandığında (bir kez). | Tüm kaynaklar birer değer yaydığında (eşleşme başına). | Herhangi bir kaynak değer yaydığında (tüm kaynaklar ilk değerini yaydıktan sonra). | Sadece kaynak Observable değer yaydığında. |
| Yayılan Değer | Her Observable’dan son değer. | Her Observable’dan eşleşen değer. | Her Observable’dan en son değer. | Kaynak Observable’dan gelen değer + diğerlerinden en son değerler. |
| Çıktı Tipi | Dizi veya nesne (tek bir değer). | Dizi veya projeksiyon fonksiyonu çıktısı. | Dizi veya projeksiyon fonksiyonu çıktısı. | Dizi veya projeksiyon fonksiyonu çıktısı. |
| Tamamlanma | Tüm kaynaklar tamamlandığında tamamlanır. | En kısa kaynak tamamlandığında tamamlanır. | Tüm kaynaklar tamamlandığında tamamlanır. | Kaynak Observable tamamlandığında tamamlanır. |
| Hata Durumu | Herhangi bir kaynak hata verirse hemen hata yayar. | Herhangi bir kaynak hata verirse hemen hata yayar. | Herhangi bir kaynak hata verirse hemen hata yayar. | Herhangi bir kaynak hata verirse hemen hata yayar. |
| Kullanım Yeri | Paralel API çağrıları, tüm verinin hazır olması. | Sıralı eşleştirme, senkronizasyon. | Dinamik UI, form izleme, filtreleme. | Olayları bağlamla zenginleştirme, durum yakalama. |
Ne Zaman Hangisini Kullanmalı?
* forkJoin kullanın, eğer:
Birden fazla Observable’ın tamamlanmasını beklemek ve her birinden gelen son* değerleri tek bir seferde almak istiyorsanız.
* Tüm verilerin yüklenip hazır olduğundan emin olmanız gereken “hepsi ya da hiçbiri” senaryolarınız varsa (örneğin, bir sayfanın tüm başlangıç verileri).
* Promise.all() benzeri bir davranış arıyorsanız.
* zip kullanın, eğer:
Birden fazla Observable’dan gelen değerleri sırasına göre* eşleştirmek istiyorsanız.
* Observable’lar arasında bir “senkronizasyon noktası” oluşturmanız gerekiyorsa ve en kısa akışın bitiminde işlemin sonlanması uygunsa.
* Her iki akışın da aynı sayıda öğe yayacağı veya eşleşmeyen öğelerin göz ardı edilmesinin kabul edilebilir olduğu durumlarda.
* combineLatest kullanın, eğer:
Birden fazla Observable’dan gelen en son* değerleri sürekli olarak izlemek ve herhangi bir Observable değer yaydığında güncellenmiş birleşik bir durum elde etmek istiyorsanız.
* Dinamik kullanıcı arayüzleri, form girişlerinin validasyonu veya birden fazla filtrenin birleşimi gibi sürekli güncellenen durumları yönetmeniz gerekiyorsa.
* Tüm Observable’ların ilk değerlerini yaydıktan sonra sürekli olarak tetiklenmesini bekliyorsanız.
* withLatestFrom kullanın, eğer:
Belirli bir “kaynak” Observable’dan gelen bir olayı, diğer Observable’lardan gelen o anki en son bağlamsal verilerle* birleştirmek istiyorsanız.
* Diğer Observable’ların kendi başlarına bir çıktı tetiklemesini istemiyor, sadece kaynak Observable tetiklendiğinde onların en son durumunu kullanmak istiyorsanız.
* Bir kullanıcı eylemini (kaynak) uygulama durum verileriyle (diğerleri) zenginleştirmeniz gerekiyorsa.
Sonuç
RxJS’in forkJoin, zip, combineLatest ve withLatestFrom operatörleri, zaman uyumsuz veri akışlarını birleştirme ve senkronize etme konusunda güçlü araçlar sunar. Her bir operatörün kendine özgü bir davranış biçimi ve belirli senaryolar için en uygun olduğu durumlar vardır.
Bu operatörleri doğru bir şekilde anlamak ve uygulamalarınızda bilinçli bir şekilde kullanmak, daha temiz, daha öngörülebilir ve bakımı daha kolay reaktif kod yazmanıza olanak tanır. Uygulamanızın ihtiyaçlarını dikkatlice analiz ederek ve her bir operatörün tetikleme mekanizmasını, değer yayma stratejisini ve tamamlanma koşullarını göz önünde bulundurarak, RxJS’in sunduğu bu zenginliği en verimli şekilde kullanabilirsiniz. Unutmayın, reaktif programlama bir zihniyet değişikliği gerektirir ve bu operatörler, bu zihniyeti uygulamaya dökmenin temel taşlarıdır.
