Skip to content

Bölüm 9 / 13

Signals ve modern Angular

Değişiklik algılama.

11 dakikalık okuma

Sekiz derstir cevabını ertelediğimiz soru şu: alan değiştiğinde ekranın güncellendiğini Angular nereden biliyor? Bu ders önce o mekanizmayı — zone.js ve değişiklik algılamayı — açar, 2. dersten borç kalan OnPush'u kapatır; sonra Angular'ın yeni cevabını kurar: signal — "değerim değişti" diyebilen değer. signal / computed / effect üçlüsü, model() ve RxJS ile işbölümü; 5. dersteki BehaviorSubject servis kalıbının sadeleşmiş hâliyle bitireceğiz.

01Ne öğreneceksin

  • Değişiklik algılama: zone.js ne yapar, varsayılan strateji neyi öder
  • OnPush — 2. dersin senedi: ne zaman çizer, değişmezlik sözleşmesi neden asıl burada kapanır
  • signal / computed / effect; set-update ve eşitlik kuralı
  • model() ile iki yönlü bileşen bağı
  • RxJS vs signals karar tablosu ve toSignal/toObservable köprüsü
  • Servis durum kalıbının signal hâli; zoneless yön

02Perde arkası: zone.js ve değişiklik algılama

8. hatta DOM'u elle güncelliyordun: veriyi değiştir, sonra textContent'e yaz. Angular'da ikinci adımı hiç yazmadın — çünkü çerçeve, şablonla sınıf arasındaki bağları biliyor ve "uygun anda" hepsini yeniden değerlendiriyor. Soru o "uygun anın" nasıl seçildiği. Klasik cevap zone.js'tir: uygulama açılırken tarayıcının asenkron API'lerini — setTimeout, olay dinleyicileri, HTTP cevapları, Promise'ler — sarmalayarak araya girer. Böylece Angular, "bir asenkron iş bitti" haberini alır ve şu varsayımla davranır: asenkron bir şey olduysa, durum değişmiş olabilir → bileşen ağacını tepeden tırnağa gezip şablon bağlarını yeniden değerlendirir, değişen yerleri DOM'a işler.

Bu tasarımın gücü şeffaflıktır: sekiz ders boyunca alanı değiştirdin, ekran "kendiliğinden" güncellendi. Bedeli iki yönlüdür:

  • Kaba tetik: zone.js ne değiştiğini bilmez, yalnız bir şeylerin olduğunu bilir. Alakasız bir setTimeout bile tüm ağacın kontrolünü tetikler.
  • Kaba tarama: varsayılan stratejide kontrol, bütün bileşen ağacını gezer. Küçük uygulamada hissedilmez; binlerce bağı olan büyük ekranda her tetikte ödenen gerçek bir maliyettir.

03OnPush: 2. dersin senedi kapanıyor

İlk optimizasyon kapısı, bileşeni taramadan muaf tutmayı bilinçli kurala bağlamaktır:

@Component({
  selector: "app-basvuru-karti",
  changeDetection: ChangeDetectionStrategy.OnPush,
  /* … */
})

OnPush'lu bileşen, şu durumlar dışında kontrol edilmez: (1) bir input'unun referansı değişti; (2) kendi şablonundan bir olay doğdu (tıklama, girdi); (3) async pipe'ına yeni değer geldi — ya da birazdan göreceğin gibi, okuduğu bir signal değişti. Sunumsal bileşenler (2. dersin ayrımı) bu sözleşmeye zaten uyar: dışarıdan veri alır, olay yayar — OnPush onlar için bedava kazançtır.

Ve işte üç derstir taşıdığımız değişmezlik sözleşmesinin asıl gerekçesi: "referansı değişti" satırını tekrar oku. Diziye push ile eleman eklersen dizi aynı nesnedir — referans değişmedi, OnPush'lu liste bileşeni kontrol edilmez, ekran güncellenmez. "Veriyi ekledim, liste yenilenmiyor; ama pencereye tıklayınca birden görünüyor" — bu tarif, sahada OnPush + mutasyon vakasının parmak izidir (tıklama olayı kontrolü tetikler, gecikmiş güncelleme o an işlenir). [...liste, yeni] yazımı stil değil, çalışma garantisidir: 3. derste saf pipe'lar, 5. derste BehaviorSubject güncellemeleri, şimdi OnPush — hepsi aynı sözleşmeye yaslanıyor: durumu değiştirme, yenisiyle değiştir.

04Signal: "değişti" diyebilen değer

Zone yaklaşımının kökündeki eksik şu: sıradan bir alan, değiştiğini haber veremez — bu yüzden çerçeve tahminle tarar. Signal bu eksiği kapatır: değeri saran, okuyanları kaydeden ve değişince tam olarak onları uyaran küçük bir kutu.

import { signal, computed, effect } from "@angular/core";

export class BasvuruListesi {
  filtre     = signal("");
  basvurular = signal<Basvuru[]>([]);

  // computed: bağımlılıkları OTOMATİK izlenen türev değer
  gorunenler = computed(() =>
    this.basvurular().filter((b) => b.unvan.includes(this.filtre())));
  sayi = computed(() => this.gorunenler().length);

  ekle(b: Basvuru) {
    this.basvurular.update((liste) => [...liste, b]);  // değişmez güncelleme — hâlâ
  }
  filtrele(metin: string) { this.filtre.set(metin); }
}
<input (input)="filtrele($any($event.target).value)">
<p>{{ sayi() }} başvuru</p>
@for (b of gorunenler(); track b.id) { <app-basvuru-karti [basvuru]="b" /> }

Okuma çağrıdır: filtre() — çağrı anında signal, "beni bu şablon okuyor" kaydını düşer; set/update geldiğinde çerçeve hangi görünümlerin bayatladığını tahminsiz bilir. Zincir de kendiliğinden kurulur: filtre değişti → gorunenler bayatladı → sayi bayatladı → yalnız bu ikisini okuyan yerler güncellenir. Ayrıntılar:

  • set(yeni) düz atama, update(fn) mevcut değerden türetme — koleksiyonlarda hep update + kopya (push değil; signal varsayılanda referans eşitliğine bakar, yerinde değişimi değişiklik saymaz — OnPush'takiyle aynı tuzak, aynı çözüm).
  • computed tembel ve önbelleklidir: kimse okumazsa hesaplanmaz; bağımlılıkları değişmedikçe eski sonucu döndürür. "Türetilebilir değeri alanda tutma, computed'la türet" — filtrelenmiş liste + ayrıca tutulan sayaç gibi çelişebilen çiftler bu kuralla ortadan kalkar.
  • effect(() => ...) yan etki içindir: okuduğu signal'lar değişince yeniden koşar — günlükleme, localStorage'a yazma, başlık güncelleme. Sınırını çiz: effect içinde signal'a yazmak refleks olmasın — iki effect birbirini tetikleyip döngü kurabilir; türev değer computed'un işidir, effect son çaredir. (İçeride bir okumayı izlemeden yapmak gerekirse untracked vardır — varlığını bil.)

05Bileşen API'si: input() zaten oradaydı, model() yeni

2. dersten beri kullandığın input() ve output()'un sırrı şimdi açıldı: input() bir signal döndürür — bu yüzden şablonda basvuru() diye çağırıyordun. Router'ın parametre bağlaması (7. ders) da aynı kapıya akar: parametre değişir, input signal'ı güncellenir, ona bağlı computed'lar zinciri yeniler — "bileşen yeniden kullanılır" tuzağının modern çözümü kendiliğinden kurulur:

export class BasvuruDetayi {
  id = input.required<string>();                 // rota parametresi — 7. ders
  basvuru = computed(() => this.servis.bul(this.id()));  // id değişti → yeniden bulundu
}

model() ise iki yönlü bağın signal hâlidir: hem içeriden hem dışarıdan yazılabilen input —

export class AramaKutusu {
  metin = model("");                    // hem input hem output
  temizle() { this.metin.set(""); }     // içeriden yazınca üst bileşen de görür
}

// üst şablon: <app-arama-kutusu [(metin)]="aramaMetni" />

Perde arkası veri-aşağı-olay-yukarı'dan (2. ders) ibarettir: [(metin)], [metin] girişi + (metinChange) çıkışının kısaltmasıdır. model() bu çifti tek satıra indirir; kullanım yeri, gerçekten iki yönlü olan yerel değerlerdir (arama metni, açık/kapalı hâli) — paylaşılan uygulama durumu değil (o, servistedir).

06İşbölümü: RxJS vs signals — ve köprü

Şimdi eldeki iki reaktif araca birlikte bak. İkisi de "değişince tepki ver" der; farkları eksenleridir:

Observable (RxJS)Signal
DoğasıZaman içinde olaylar — akışŞu anki değer — durum
Şimdiki değerYok (BehaviorSubject hariç)Her zaman var: x()
Zaman operatörleridebounce, switchMap, yarış çözümü…Yok — derdi zaman değil
TüketimAbonelik ister, sızıntı disiplini isterOkumak yeter; abonelik/sızıntı derdi yok
Doğru işiHTTP, kullanıcı olayları, WebSocket, form akışıEkran durumu, türev değerler, paylaşılan durum

Ezber cümlesi: zaman ve olay → RxJS; durum ve türev → signal. Arama kutusu iki dünyayı aynı özellikte buluşturur: yazma olayları + debounce + istek yarışı RxJS'in işidir; sonuç listesi ve "kaç sonuç" bilgisi signal'ın. Köprü hazırdır:

import { toSignal } from "@angular/core/rxjs-interop";

sonuclar = toSignal(
  this.aramaMetni$.pipe(debounceTime(300), distinctUntilChanged(),
    switchMap((m) => this.servis.ara(m).pipe(catchError(() => of([]))))),
  { initialValue: [] as Basvuru[] },   // signal'ın HER AN değeri olmalı — zorunlu karar
);

toSignal aboneliği kendi kurar, bileşenle birlikte bırakır (sızıntı disiplini içeride). initialValue rastgele bir ayrıntı değildir: akış ilk değerini zamanla üretir, signal ise her an okunabilir olmalıdır — ilk değeri sen seçmek zorundasın. Ters köprü toObservable'dır: bir signal'ı akışa çevirip RxJS operatörlerine sokar.

Servis kalıbının sadeleşmesi — 5. dersin sözü: BehaviorSubject + asObservable() + async pipe üçlüsü, signal'da üç kavram birden eksilterek kurulur (Subject yok, abonelik yok, pipe yok):

@Injectable({ providedIn: "root" })
export class BasvuruDurumu {
  private readonly _basvurular = signal<Basvuru[]>([]);
  readonly basvurular = this._basvurular.asReadonly();   // dışarıya salt okunur yüz
  readonly sayi = computed(() => this._basvurular().length);

  ekle(b: Basvuru) { this._basvurular.update((l) => [...l, b]); }
}

Disiplinler yerinde duruyor: yazma yetkisi private signal'da, dışarıya asReadonly(); güncelleme değişmez. Yeni projede durum kalıbının varsayılanı budur; 11. hatta göreceğin durum yönetimi kütüphaneleri ve React karşılaştırması bu temelin üstüne oturacak.

07Zoneless yön ve üretim pratiği

Signal'lar yaygınlaşınca zone.js'in varlık sebebi eriyor: çerçeve neyin değiştiğini artık tahmin etmek yerine biliyor. Angular bu yüzden zoneless moda yürüyor (provideZonelessChangeDetection()): zone.js paketi düşer, alakasız asenkron işler boşuna tarama tetiklemez; güncelleme sinyal tabanlı olur. Yeni projelerde yön budur; geçiş, "durumu signal'da tut" disiplinini baştan kurmuş kod için ucuzdur. Günlük kurallar:

  • Yeni bileşen durumu signal ile başlar; düz alan + "Angular nasılsa görür" varsayımı, zoneless dünyada kırılan koddur.
  • Türev değer alanda tutulmaz — computed ile türetilir; senkron tutulması gereken ikinci kopya, bir gün mutlaka çelişir.
  • effect azdır: ekrana yansıyan her şey signal/computed zinciriyle akar; effect yalnız dış dünyaya sızan yan etkiler içindir.
  • RxJS yerinde kalır: signals RxJS'in yerine değil yanına geldi — HTTP, debounce, yarış çözümü hâlâ operatör işidir; sınırda toSignal köprüsü kurulur.

08Mini alıştırma — BehaviorSubject'ten signal'a

Yaklaşık 20 dakika · kurulum yok — StackBlitz'te Angular şablonu; 5. dersin alıştırmasındaki durum servisini taban al

  1. filtre ve basvurular signal'larıyla başla; gorunenler ve sayi'yı computed ile türet. Şablonda üçünü de göster; filtre yazdıkça zincirin akışını izle.
  2. Mutasyon deneyi: ekle'yi önce update(l => { l.push(b); return l; }) diye yaz — listenin ekranda güncellenmediğini gör (aynı referans döndü). [...l, b]'ye çevir, düzeldiğini doğrula; farkı bir cümleyle yaz.
  3. effect ekle: filtre her değiştiğinde localStorage'a yazsın; açılışta signal'ı oradan başlat. Konsola da loglayıp effect'in yalnız okuduğu signal değişince koştuğunu gözle.
  4. 5. dersteki BasvuruDurumu servisini (BehaviorSubject + asObservable) signal kalıbına taşı: private signal + asReadonly + computed. Bileşenlerden | async'lerin silindiğini, çağrıya döndüğünü gör — satır sayısını kıyasla.
  5. model() ile AramaKutusu bileşeni yaz; üstte [(metin)] ile bağla, içindeki "temizle" düğmesinin üst bileşendeki değeri de sıfırladığını doğrula.
  6. Köprü: arama metnini toObservable ile akışa çevir, debounceTime(300) + sahte servis switchMap'i uygula, sonucu toSignal(..., { initialValue: [] }) ile geri al. Hangi parçanın neden hangi dünyada olduğunu bir cümleyle yaz.

09Kendini sına

Angular ekranın güncelleneceğini nereden bilir? zone.js'in rolünü ve varsayılan stratejinin maliyetini anlat.

Klasik mekanizma iki parçadır: tetik ve tarama. Tetiği zone.js sağlar: uygulama açılırken tarayıcının asenkron API'lerini — zamanlayıcılar, olay dinleyicileri, HTTP cevapları, Promise'ler — sarmalar; bir asenkron iş bittiğinde Angular haber alır ve "durum değişmiş olabilir" varsayımıyla değişiklik algılamayı başlatır. Tarama ise varsayılan stratejide bileşen ağacının tamamını gezip şablon bağlarını yeniden değerlendirmek ve farkları DOM'a işlemektir. Gücü şeffaflıktır — geliştirici alanı değiştirir, gerisi kendiliğinden olur; maliyeti kabalığıdır: zone.js ne değiştiğini bilmez, alakasız bir setTimeout bile tüm ağacın taranmasını tetikler; büyük ekranlarda bu her tetikte ödenen gerçek bir bedeldir. İyileştirme merdiveni: OnPush ile bileşenleri sözleşmeli biçimde taramadan muaf tutmak; signals ile değişikliği tahmin yerine bildirim hâline getirmek; ve nihai adım zoneless — signal tabanlı güncellemeyle zone.js'i tamamen devreden çıkarmak.

OnPush nedir; OnPush'lu bileşen hangi durumlarda kontrol edilir? "Listeye ekledim, ekran güncellenmedi" vakasını teşhis et.

OnPush, bileşen için değişiklik algılama sözleşmesini sıkılaştırır: bileşen yalnız şu durumlarda kontrol edilir — bir input'unun referansı değişirse, kendi şablonundan bir olay doğarsa, async pipe'ına yeni değer gelirse ya da okuduğu bir signal değişirse. Sunumsal bileşenler bu sözleşmeye doğal uyar; büyük listelerde kazanç ölçülebilirdir. Vakanın teşhisi: liste bileşeni OnPush'tur ve veri push ile eklenmiştir — dizi aynı nesne olduğundan input referansı değişmemiş, çerçeve bileşeni kontrol etmemiştir. Parmak izi şudur: ekran güncellenmez ama kullanıcı bir yere tıklayınca (olay, kontrolü tetikler) birikmiş değişiklik aniden görünür. Çözüm değişmezliktir: [...liste, yeni] gibi kopya üreten güncellemeler — referans değişir, OnPush görür. Bu, saf pipe'ların ve signal eşitlik kuralının da aynı gerekçesidir: çerçevenin ucuz değişiklik testi referans karşılaştırmasıdır; yerinde değiştiren kod, bu testi kör eder. "Değiştirme, yenisiyle değiştir" sözleşmesi stilistik değil işlevseldir.

signal, computed, effect nedir? computed'ın özellikleri ve effect'in tuzağı nelerdir?

signal, değeri saran ve okuyanlarını izleyen reaktif kutudur: x() ile okunur — okuma, bağımlılık kaydı düşürür; set ile atanır, update ile mevcut değerden türetilir. Varsayılan eşitlik referans karşılaştırmasıdır: koleksiyonda yerinde değişim "değişiklik" sayılmaz, güncellemeler kopya üretir. computed, başka signal'lardan türeyen değerdir; bağımlılıklarını çalışırken otomatik keşfeder, tembeldir (okunmazsa hesaplanmaz) ve önbelleklidir (bağımlılık değişmedikçe eski sonucu verir). Değeri, çelişebilen ikinci kopyaları yok etmesidir: filtrelenmiş liste, toplam, seçili öğe — hepsi alan değil türevdir. effect, okuduğu signal'lar değişince yeniden koşan yan etki bloğudur: günlük, localStorage, belge başlığı gibi dış dünyaya sızan işler için. Tuzağı, içinde duruma yazmaktır: signal'a yazan effect'ler birbirini tetikleyip döngü ve izlenmesi zor akışlar kurar — türev değer computed'un işidir, effect son çaredir; istisnai durumda bir okumayı izlemeden yapmak için untracked vardır. Bileşen API'si de aynı zemindedir: input() signal döndürür, model() iki yönlü bağın signal hâlidir.

RxJS ile signals arasında işbölümünü nasıl kurarsın? Servis durum kalıbının iki sürümünü kıyasla.

Karar ekseni zamandır: olay ve akış RxJS'in, durum ve türev signal'ındır. HTTP istekleri, kullanıcı olayları, debounce, yarış çözümü (switchMap) — bunlar zaman içinde dizilen olaylardır, operatör ister; ekranın şu anki hâli, filtrelenmiş liste, sayaçlar — bunlar her an okunabilir durumdur, signal ister. Tipik özellik ikisini birleştirir: arama kutusunda yazma olayları RxJS zincirinden geçer, sonuç toSignal ile duruma iner — initialValue zorunlu karardır çünkü akış ilk değerini zamanla üretir ama signal her an okunabilir olmalıdır; ters yön toObservable'dır. Servis kalıbı kıyası: RxJS sürümünde private BehaviorSubject + asObservable() ile salt okunur yüz + tüketicide async pipe ve abonelik disiplini vardır; signal sürümünde private signal + asReadonly() + computed türevleri — Subject, abonelik ve pipe kavramları tümden düşer, tüketici değeri çağrıyla okur. Değişmeyenler daha önemlidir: yazma yetkisi hâlâ serviste kapsüllüdür, güncellemeler hâlâ değişmezdir. Signals RxJS'in yerine değil yanına gelmiştir; zoneless Angular'ın da temelidir — çerçeve neyin değiştiğini tahmin etmek yerine bilir.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Angular'da değişiklik algılama nasıl çalışır; zone.js'in görevi nedir? (2) Varsayılan strateji ile OnPush arasındaki fark nedir? (3) OnPush'lu bileşen hangi durumlarda güncellenir? (4) Değişmez veri güncellemesi OnPush için neden zorunludur? (5) signal, computed, effect kavramlarını açıklayınız. (6) computed'ın tembellik ve önbellek özellikleri ne kazandırır? (7) effect içinde duruma yazmak neden sakıncalıdır? (8) model() nedir; hangi ikilinin kısaltmasıdır? (9) Hangi işte RxJS, hangi işte signal tercih edilir; toSignal'da initialValue neden zorunludur? (10) Zoneless Angular ne demektir; neyi mümkün kılar?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Mekanizma — "Klasik Angular'da zone.js asenkron işleri sarar, her bitişte ağaç taranır: şeffaf ama kaba." (2) OnPush — "Sözleşmeyi sıkarım: referans değişimi, kendi olayı, async pipe, signal; bedeli değişmezlik disiplinidir — push edilen liste ekranı güncellemez, parmak izini bilirim." (3) Signals — "Signal, değişti diyebilen değerdir: set/update, computed türev — tembel ve önbellekli, effect yalnız dış yan etki; çerçeve tahmin yerine bilir." (4) İşbölümü — "Zaman/olay RxJS, durum/türev signal; arama kutusunda debounce + switchMap akışta, sonuç toSignal ile durumda — initialValue bilinçli karar." (5) Kıdem vuruşu — "Servis kalıbım: private signal + asReadonly + computed — BehaviorSubject üçlüsünün sade hâli; yön zoneless, o yüzden durumu baştan signal ile kurarım."