Ders 08 / 25
Yan Etkiler
Bileşenin dış dünyayla eşzamanlanması; oluşturmanın saflığı, yan etki kaydının belgeye yazma aşamasından sonra çalışması, bağımlılık listesinin karşılaştırılması, temizleme işlevinin sırası ve eşzamansız etkilerde yarış koşulu.
İçindekiler
Önceki ders bileşen gövdesinin tek bir işi olduğunu kurdu: durumdan ve girdilerden bir çıktı hesaplamak. Ölçüm istasyonu sayfasının yapması gereken işlerin bir bölümü bu hesabın dışında kalır. Sunucudan ölçüm çekmek, ölçüm akışına abone olmak, seçili süzgeci yerel depolamaya yazmak, belge başlığını değiştirmek, bir zamanlayıcı kurmak — hiçbiri bir çıktı hesabı değildir. Bunlar programın dışarıya dokunduğu noktalardır ve Programlama Temelleri kursunda tanıtılan adla yan etki (side effect) taşırlar.
Bu derste sorulan soru, bu işlerin nereye yazılacağıdır. Yanıt bir kayıt mekanizmasıdır: gövde yan etkiyi yapmaz, yapılacak işi bildirir. Çalışma zamanı bu bildirimi alır, doğru anda çalıştırır ve — bu dersin en çok atlanan yarısı — kurulan şeyi doğru anda söker.
Oluşturmanın Saflığı
Yan etkinin gövdeye yazılamamasının üç ayrı gerekçesi vardır ve üçü de gövdenin bir sözleşmeye bağlı olmasından gelir: aynı girdilerle çağrılan gövde aynı çıktıyı üretmeli ve başka hiçbir şey yapmamalıdır.
Birincisi zamanlamadır. Gövdenin ürettiği ağaç henüz belgeye yazılmamıştır; uzlaşma sonucundaki fark daha uygulanmamıştır. Gövde içinde bir düğümü ölçmeye ya da odaklamaya çalışan kod, olmayan bir düğümle konuşur.
İkincisi iptal edilebilirliktir. Bir oluşturma yarıda kesilebilir; çalışma zamanı daha öncelikli bir güncelleme gelince başlanmış hesabı bırakıp baştan başlayabilir. Gövde içinde gönderilen bir istek, sonucu hiç kullanılmayacak bir istektir; gövde içinde artan bir sayaç iki kez artmış olabilir.
Üçüncüsü çoğaltmadır. Aynı gövde, geliştirme sırasındaki denetimler ya da bir sunucu tarafı oluşturma turu nedeniyle iki kez çalıştırılabilir. Saf bir gövde için bu görünmez bir işlemdir; abonelik açan bir gövde için iki aboneliktir.
Bu üç gerekçe tek bir kurala iner: gövde okur ve hesaplar; dışarıya dokunan her iş kayıtla bildirilir.
Yan Etki Kaydı ve Bağımlılık Listesi
Gövdenin bildirdiği işe yan etki kaydı (effect) denir. Kayıt iki parçadan oluşur: çalışacak işlev ve bir bağımlılık listesi (dependency list). Çalışma zamanı, hesaplanan farkı belgeye uyguladıktan sonra — bu aşamaya bu kursta belgeye yazma aşaması (commit) denir — kayıtları gözden geçirir ve listesi bir önceki oluşturmadakinden farklı olanları çalıştırır.
Karşılaştırma öge tek öge kimlik karşılaştırmasıdır; derin bir eşitlik sınaması değildir. Bunun sonuçları bu dersin sonunda ele alınır.
// yan-etki.mjs — bagimlilik listesi, calisma kosulu ve temizleme sirasi let etkin = null; const yaz = (s) => console.log(" " + s); function yanEtkiKaydi(islev, bagimliliklar) { etkin.bekleyen.push({ i: etkin.imlec++, islev, bagimliliklar }); } // Liste verilmemisse her islemede calisir; verilmisse ogeler tek tek karsilastirilir. function ayniMi(a, b) { if (a === undefined || b === undefined) return false; return a.length === b.length && a.every((v, k) => Object.is(v, b[k])); } function olustur(o, ozellikler) { etkin = o; o.imlec = 0; o.bekleyen = []; console.log(`olusturma: istasyon=${ozellikler.istasyon}`); o.govde(ozellikler); etkin = null; for (const kayit of o.bekleyen) { // islem asamasi: cizimden sonra const onceki = o.etkiler[kayit.i]; if (onceki && ayniMi(onceki.bagimliliklar, kayit.bagimliliklar)) continue; if (onceki && onceki.temizle) onceki.temizle(); kayit.temizle = kayit.islev() || null; o.etkiler[kayit.i] = kayit; } } function sok(o) { console.log("agactan cikarma"); for (const k of o.etkiler) if (k && k.temizle) k.temizle(); o.etkiler = []; } const panel = { etkiler: [], imlec: 0, bekleyen: [] }; panel.govde = ({ istasyon }) => { yanEtkiKaydi(() => { yaz("A calisti (liste yok)"); }); yanEtkiKaydi(() => { yaz("B calisti (bos liste)"); return () => yaz("B temizlendi"); }, []); yanEtkiKaydi(() => { yaz(`C calisti: ${istasyon} akisina abone`); return () => yaz(`C temizlendi: ${istasyon} aboneligi kesildi`); }, [istasyon]); }; olustur(panel, { istasyon: "KY-01" }); olustur(panel, { istasyon: "KY-01" }); olustur(panel, { istasyon: "KY-02" }); sok(panel);
olusturma: istasyon=KY-01 A calisti (liste yok) B calisti (bos liste) C calisti: KY-01 akisina abone olusturma: istasyon=KY-01 A calisti (liste yok) olusturma: istasyon=KY-02 A calisti (liste yok) C temizlendi: KY-01 aboneligi kesildi C calisti: KY-02 akisina abone agactan cikarma B temizlendi C temizlendi: KY-02 aboneligi kesildi
Listenin üç biçimi çıktıda ayrı ayrı okunur.
Liste verilmemişse etki her belgeye yazma aşamasından sonra çalışır. A kaydı dört oluşturmanın üçünde de çalışmıştır. Bu biçim, eşzamanlanacak değer belirsiz olduğunda seçilir ve pahalıdır.
Boş liste verilmişse etki yalnız ilk oluşturmadan sonra çalışır. B kaydı bir kez çalışmış ve yalnız ağaçtan çıkarılırken temizlenmiştir. Bileşenin ömrü boyunca bir kez kurulacak işler — bir gözlemci kurmak, belgeye bir dinleyici bağlamak — bu biçimi kullanır.
Değer içeren liste verilmişse karşılaştırma sonucu belirleyicidir. İkinci oluşturmada istasyon değişmediği için C kaydı hiç çalışmamıştır; üçüncüde değiştiği için çalışmıştır.
Temizleme
Yan etkinin ikinci yarısı, kurduğu şeyi sökmesidir. Kayıt işlevi bir işlev döndürürse, bu işlev temizleme işlevi (cleanup function) olarak saklanır ve iki anda çağrılır: etki yeniden çalışmadan hemen önce ve bileşen ağaçtan çıkarılırken.
Üçüncü oluşturmadaki sıra bu yüzden önemlidir. Önce eski abonelik kesilmiş, sonra yenisi açılmıştır. Ters sıra iki aboneliğin bir an için birlikte var olması demek olurdu; abone sayısını sayan bir sunucu için bu ölçülebilir bir farktır. Temizlemenin atlanması ise istasyon her değiştiğinde bir abonelik biriktirir. Bu birikim iki maliyet üretir: kapalı kalmayan bağlantılar ve — Eşzamansız JavaScript ve Çalışma Zamanı kursunda tanımlanan sızıntı — artık ağaçta olmayan bir bileşene erişim tutan geri çağırımlar.
Ağaçtan çıkarmada iki temizleme de çalışmıştır. Bu, temizlemenin sözleşmesini tanımlar: etkinin kurduğu her şeyin karşılığı temizlemede sökülmelidir. Dinleyici bağlayan etki dinleyiciyi kaldırır, zamanlayıcı kuran etki zamanlayıcıyı iptal eder, abone olan etki aboneliği keser. Temizlemenin kendisi de Kabuk Programlama kursundaki temizlik kancaları gibi yinelemeye dayanıklı yazılır; zaten kapalı bir bağlantıyı ikinci kez kapatmak hata üretmemelidir.
Eşzamansız Etkide Yarış
Temizlemenin en az anlaşılan kullanımı ağ isteklerindedir. Bir istek iptal edilemese bile sonucunun kullanılması engellenebilir.
// yaris.mjs — eszamansiz yan etkide yanit sirasinin bozulmasi function kanal() { const bekleyen = new Map(); return { getir: (ad) => new Promise((coz) => bekleyen.set(ad, coz)), yanitla: (ad) => bekleyen.get(ad)(`${ad} olcumleri`), }; } const bosalt = () => new Promise((coz) => setTimeout(coz, 0)); async function senaryo(koruma) { console.log(koruma ? "temizleme var:" : "temizleme yok:"); const ag = kanal(); let gorunum = "-"; let temizle = null; // Bagimlilik listesi [istasyon] degistiginde calisan yan etki. const etki = (istasyon) => { if (temizle) temizle(); // onceki etkinin temizlenmesi let iptal = false; ag.getir(istasyon).then((veri) => { if (koruma && iptal) { console.log(` ${veri} geldi, atildi`); return; } gorunum = veri; console.log(` gorunum <- ${veri}`); }); temizle = () => { iptal = true; }; }; etki("KY-01"); // kullanici once KY-01 seciyor etki("KY-02"); // hemen ardindan KY-02 seciyor ag.yanitla("KY-02"); // yakin istasyon once yanitladi await bosalt(); ag.yanitla("KY-01"); // uzak istasyon sonra yanitladi await bosalt(); console.log(` secili istasyon KY-02, ekranda: ${gorunum}`); } await senaryo(false); await senaryo(true);
temizleme yok: gorunum <- KY-02 olcumleri gorunum <- KY-01 olcumleri secili istasyon KY-02, ekranda: KY-01 olcumleri temizleme var: gorunum <- KY-02 olcumleri KY-01 olcumleri geldi, atildi secili istasyon KY-02, ekranda: KY-02 olcumleri
Kullanıcı iki istasyonu hızlı seçmiş, ikinci istek birinciden önce yanıtlanmıştır. Bu bir yarış koşuludur (race condition): sonuç, isteklerin gönderilme sırasına değil dönüş sırasına bağlı kalmıştır. Temizleme olmayan durumda ekranda seçili olmayan istasyonun ölçümleri durur ve hiçbir hata iletisi yoktur.
Çözüm, her etki çalışmasının kendi geçerlilik bayrağını taşımasıdır. Temizleme bayrağı düşürür; geç gelen yanıt bayrağı sınayıp kendini atar. Aynı kalıbın taşınabilir biçimi Eşzamansız JavaScript ve Çalışma Zamanı kursundaki iptal işaretiyle kurulur: temizleme isteği iptal eder, geç gelen yanıt hiç üretilmez.
Üç Yaygın Hata
Eksik bağımlılık. Listeye yazılmayan bir değer, etkinin ilk çalıştığı oluşturmadaki kapanışta donar. Etki yeniden çalışmadığı için o değer hiç güncellenmez; bu eskimiş kapanıştır (stale closure). Kural tektir: etkinin gövdesinde okunan her değişken değer listeye girer. Listeyi kısaltarak etkinin çalışma sayısını azaltmak, hatayı gizlemekten başka bir şey yapmaz.
Her oluşturmada yeniden üretilen bağımlılık. Karşılaştırma kimlik karşılaştırması olduğu için, gövde içinde yeniden kurulan bir nesne ya da dizi her oluşturmada farklı görünür ve etki her seferinde çalışır. Bu, listeyi hiç vermemekle aynı sonuca varır.
Durumu yan etkiyle türetmek. Bir değeri başka bir değerden hesaplayıp sonucu duruma yazan etki, tek bir kullanıcı eyleminden iki oluşturma üretir ve arada tutarsız bir kare gösterir. Bu hata o kadar yaygındır ki sonraki dersin tamamı buna ayrılmıştır.
Özet
- Bileşen gövdesi saftır: okur ve hesaplar; dışarıya dokunan iş gövdede yapılmaz, kayıtla bildirilir.
- Yan etki kaydı, belgeye yazma aşamasından sonra çalışır; bağımlılık listesi bir önceki oluşturmanınkiyle öge tek öge kimlik karşılaştırmasına sokulur.
- Liste verilmemişse etki her yazma aşamasından sonra, boş listeyle yalnız ilk kez, değerli listeyle yalnız değerler değiştiğinde çalışır.
- Temizleme işlevi etki yeniden çalışmadan önce ve ağaçtan çıkarmada çağrılır; etkinin kurduğu her şeyin karşılığı temizlemede sökülmelidir.
- Eşzamansız etkilerde yanıt sırası istek sırasından farklı olabilir; temizlemenin düşürdüğü bir geçerlilik bayrağı geç gelen yanıtı elemenin en küçük yoludur.
- Eksik bağımlılık eskimiş kapanış, her oluşturmada yeniden kurulan nesne ise sürekli çalışan etki üretir.
Sonraki Adım
Yan etki kaydı dış dünyayla eşzamanlama içindir; ne var ki en sık kurulduğu yer dış dünya değildir. Süzgeç panelinde seçili tür değiştiğinde ölçüm listesini süzen, sonucu bir duruma yazan ve böylece ikinci bir oluşturma tetikleyen kod yaygın bir kalıptır — ve yanlıştır. Süzülmüş liste bir durum değil, iki durumdan hesaplanabilen bir sonuçtur; onu ayrı bir yuvada tutmak ikinci bir doğruluk kaynağı yaratır. Sonraki ders bu ayrımı kurar: hangi değerler saklanır, hangileri her oluşturmada hesaplanır ve hesap pahalıysa sonucun saklanmasının ölçülebilir kazancı nedir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.