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-updateve eşitlik kuralımodel()ile iki yönlü bileşen bağı- RxJS vs signals karar tablosu ve
toSignal/toObservablekö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
setTimeoutbile 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 hepupdate+ kopya (pushdeğil; signal varsayılanda referans eşitliğine bakar, yerinde değişimi değişiklik saymaz — OnPush'takiyle aynı tuzak, aynı çözüm).computedtembel 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ğercomputed'un işidir, effect son çaredir. (İçeride bir okumayı izlemeden yapmak gerekirseuntrackedvardı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ğer | Yok (BehaviorSubject hariç) | Her zaman var: x() |
| Zaman operatörleri | debounce, switchMap, yarış çözümü… | Yok — derdi zaman değil |
| Tüketim | Abonelik ister, sızıntı disiplini ister | Okumak yeter; abonelik/sızıntı derdi yok |
| Doğru işi | HTTP, 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 —
computedile türetilir; senkron tutulması gereken ikinci kopya, bir gün mutlaka çelişir. effectazdı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
toSignalkö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
filtrevebasvurularsignal'larıyla başla;gorunenlervesayi'yıcomputedile türet. Şablonda üçünü de göster; filtre yazdıkça zincirin akışını izle.- Mutasyon deneyi:
ekle'yi önceupdate(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. effectekle: filtre her değiştiğindelocalStorage'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.- 5. dersteki
BasvuruDurumuservisini (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. model()ileAramaKutusubileşeni yaz; üstte[(metin)]ile bağla, içindeki "temizle" düğmesinin üst bileşendeki değeri de sıfırladığını doğrula.- Köprü: arama metnini
toObservableile akışa çevir,debounceTime(300)+ sahte servisswitchMap'i uygula, sonucutoSignal(..., { 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."
