İçeriğe geç
academia.sh

Ders 07 / 25

Yerel Durum

Bileşenin oluşturmalar arası yaşayan değeri; durum yuvalarının çağrı sırasına bağlanması, güncellemenin kuyruğa alınması, tek turdaki güncellemelerin toplu işlenmesi, eskimiş değer tuzağı ve durumun ağaçta nereye konacağı.

İçindekiler

Önceki ders ağacın nasıl güncellendiğini çözdü: iki oluşturmanın ürettiği ağaçlar karşılaştırılıyor, aradaki fark en küçük dokunuşlarla belgeye uygulanıyor. Uzlaşma bittiğinde geriye tek bir soru kalır. Karşılaştırma her zaman bir yeniden çizimden sonra yapılır — peki yeniden çizimi ne başlatır?

Yanıt, bileşenin kendi içinde tuttuğu değerdir. Kuzey Yamaç Ölçüm İstasyonu sayfasındaki süzgeç paneli hangi ölçüm türünün seçili olduğunu bilir, ölçüm tablosu hangi sütuna göre sıralandığını bilir, her ölçüm rozeti açılıp açılmadığını bilir. Bu değerler sunucudan gelmez, üst bileşenden verilmez ve sayfa yenilenene kadar yaşar. Bu derste bunlara yerel durum (local state) denir; dersin konusu durumun nerede saklandığı, bir güncellemenin ne zaman görünür olduğu ve bir turdaki üç güncellemenin kaç oluşturma ürettiğidir.

Durum Nerede Yaşar

Bir bileşenin gövdesi her oluşturmada baştan çalışan bir işlevdir. İşlev gövdesinde tanımlanan yerel değişkenler her çağrıda sıfırlanır; ikinci oluşturmada birinci oluşturmadan kalan hiçbir şey yoktur. Durumun bu gövdenin dışında bir yerde yaşaması gerekir.

Yaşadığı yer, bileşenin örneğidir. Ağaçtaki her bileşen konumuna karşılık bir örnek kaydı tutulur ve bu kayıtta bir yuva listesi bulunur. Gövde çalışırken çalışma zamanı bir imleç tutar: gövde her durum isteğinde imleci bir ilerletir ve o sıradaki yuvayı verir. İlk oluşturmada yuva yoksa başlangıç değeriyle açılır, sonraki oluşturmalarda var olan yuva okunur.

Gövdenin bu yuvaya erişmek için çağırdığı işleve durum kancası (state hook) denir. Kanca iki şey döndürür: yuvadaki güncel değer ve o yuvaya yazma isteği gönderen bir işlev. Değer okunur, yazma isteği çağrılır; ikisinin arasında doğrudan bir atama yoktur.

Güncelleme Bir İstek, Atama Değildir

Yazma işlevinin adı yanıltıcı olabilir. Çağrıldığı anda yuvaya yazmaz; bir güncelleme isteğini kuyruğa koyar ve o bileşeni kirli olarak işaretler. Kuyruk, çalışan kod bittikten sonra boşaltılır. Aşağıdaki model bu kuyruğu, kuyruğun boşaltılmasını ve boşaltmanın kaç oluşturma ürettiğini görünür kılar.

// durum-kuyrugu.mjs — yerel durum, guncelleme kuyrugu ve toplu isleme
let etkinOrnek = null;
let cizimSayisi = 0;
const kuyruk = [];

function durumKancasi(ilk) {
  const o = etkinOrnek;
  const i = o.imlec++;
  if (o.yuvalar.length === i) o.yuvalar.push(typeof ilk === "function" ? ilk() : ilk);
  return [o.yuvalar[i], (yeni) => { kuyruk.push({ o, i, yeni }); }];
}

function olustur(ornek) {
  etkinOrnek = ornek;
  ornek.imlec = 0;
  const cikti = ornek.govde();
  etkinOrnek = null;
  cizimSayisi++;
  console.log(`  cizim ${cizimSayisi}: ${cikti}`);
}

// Olay isleyicisi bittiginde calisir: kuyruk bosaltilir, her ornek bir kez cizilir.
function turuBitir() {
  const kirliler = [];
  for (const is of kuyruk) {
    const eski = is.o.yuvalar[is.i];
    is.o.yuvalar[is.i] = typeof is.yeni === "function" ? is.yeni(eski) : is.yeni;
    if (!kirliler.includes(is.o)) kirliler.push(is.o);
  }
  console.log(`  [tur sonu] kuyrukta ${kuyruk.length} guncelleme, ${kirliler.length} ornek kirli`);
  kuyruk.length = 0;
  for (const o of kirliler) olustur(o);
}

const rozet = { ad: "rozet", yuvalar: [], imlec: 0, dis: null };
rozet.govde = () => {
  const [deger, yazDeger] = durumKancasi(0);
  const [birim, yazBirim] = durumKancasi("C");
  rozet.dis = { deger, yazDeger, yazBirim };
  return `deger=${deger} birim=${birim}`;
};

console.log("A. dogrudan deger yazan uc guncelleme, tek olay icinde");
olustur(rozet);
rozet.dis.yazDeger(rozet.dis.deger + 1);
rozet.dis.yazDeger(rozet.dis.deger + 1);
rozet.dis.yazBirim("F");
console.log(`  yazmadan hemen sonra govdedeki deger: ${rozet.dis.deger}`);
turuBitir();

console.log("B. ayni islem, guncelleyici islevle");
rozet.yuvalar = [0, "C"];
cizimSayisi = 0;
olustur(rozet);
rozet.dis.yazDeger((onceki) => onceki + 1);
rozet.dis.yazDeger((onceki) => onceki + 1);
rozet.dis.yazBirim("F");
turuBitir();

console.log("C. iki ayri turda yapilan ayni iki guncelleme");
rozet.yuvalar = [0, "C"];
cizimSayisi = 0;
olustur(rozet);
rozet.dis.yazDeger((onceki) => onceki + 1);
turuBitir();
rozet.dis.yazDeger((onceki) => onceki + 1);
turuBitir();
A. dogrudan deger yazan uc guncelleme, tek olay icinde
  cizim 1: deger=0 birim=C
  yazmadan hemen sonra govdedeki deger: 0
  [tur sonu] kuyrukta 3 guncelleme, 1 ornek kirli
  cizim 2: deger=1 birim=F
B. ayni islem, guncelleyici islevle
  cizim 1: deger=0 birim=C
  [tur sonu] kuyrukta 3 guncelleme, 1 ornek kirli
  cizim 2: deger=2 birim=F
C. iki ayri turda yapilan ayni iki guncelleme
  cizim 1: deger=0 birim=C
  [tur sonu] kuyrukta 1 guncelleme, 1 ornek kirli
  cizim 2: deger=1 birim=C
  [tur sonu] kuyrukta 1 guncelleme, 1 ornek kirli
  cizim 3: deger=2 birim=C

Toplu İşleme

A bölümünün ilk okunacak satırı, yazma çağrılarından hemen sonra gövdedeki değerin hâlâ sıfır olmasıdır. Gövde, o oluşturmada okuduğu değeri bir kapanışta tutar; kapanış o oluşturmaya aittir ve yazma isteği onu değiştirmez. Bir oluşturma boyunca durum sabittir. Bu, bir kısıt değil bir güvencedir: aynı gövde içinde bir değerin bir yerde eski, başka bir yerde yeni okunması olanaksızdır.

İkinci okunacak satır oluşturma sayısıdır. Üç güncelleme kuyruğa girmiş, tur sonunda kuyruk boşaltılmış ve tek bir oluşturma yapılmıştır. Bir turdaki bütün güncellemelerin biriktirilip tek oluşturmayla karşılanmasına toplu işleme (batching) denir. Toplu işleme olmasaydı üç yazma üç oluşturma üretirdi; her oluşturma bir uzlaşma turu demek olduğu için maliyet üçe katlanırdı. Dahası ara oluşturmalar tutarsız görünürdü: değer güncellenmiş ama birim henüz eski olurdu.

Toplu işlemenin sınırı tur sınırıdır. C bölümünde aynı iki güncelleme iki ayrı turda yapılmış ve iki oluşturma üretmiştir. Bir olay işleyicisi içinde yapılan güncellemeler aynı gruba girer; bir zamanlayıcı geri çağırımından ve bir ağ yanıtından gelen güncellemeler ayrı turlardır ve genellikle ayrı oluşturmalar üretir.

Eskimiş Değer ve Güncelleyici İşlev

A ile B arasındaki tek fark yazma çağrısının biçimidir; sonuç birde ve ikide ayrılır.

A’da her iki çağrı da deger + 1 ifadesini kendi kapanışındaki sıfırdan hesaplamış, ikisi de bire yazmıştır. İkinci yazma birinciyi ezmez, aynı sonucu yeniden yazar. Bu, eskimiş değer (stale value) tuzağıdır ve tek bir turda aynı yuvaya iki kez yazan her kodda belirir: sayaç iki artırma sonunda bir artmıştır.

B’de yazma çağrısına değer değil bir işlev verilmiştir. Kuyruk boşaltılırken bu işlev, o andaki yuva değeriyle çağrılır; birinci istek sıfırdan bire, ikincisi birden ikiye taşır. Buradan bağlayıcı bir kural çıkar: yeni değer eski değere bağlıysa güncelleyici işlev yazılır. Yeni değer eskisinden bağımsızsa — süzgecin seçilen türü, panelin açık olması — doğrudan değer yazmak yeterlidir.

Çağrı Sırasının Değişmezliği

Modelde yuvalar ada göre değil imlece göre bulunur. Bunun bir bedeli vardır: gövdenin her oluşturmada durum kancasını aynı sırayla ve aynı sayıda çağırması gerekir. Kanca çağrısı bir koşulun içine alınırsa, koşul değiştiği oluşturmada imleç kayar ve ikinci yuvanın değeri birinci yuvadan okunur. Hata sessizdir; birbiriyle ilgisiz iki değer yer değiştirir.

Bu yüzden durum çağrıları gövdenin en üstünde, koşulsuz ve döngüsüz yazılır. Koşullu bir durum gerekiyorsa koşul kancanın etrafına değil, kancadan sonraki kullanıma konur. Ada göre çözümleme yapan tasarımlarda bu kısıt yoktur; buna karşılık ad çakışmalarını denetlemek gerekir. Sıraya dayanan tasarımın kazancı, hiçbir ad kaydı tutmadan sabit zamanda yuva bulmasıdır.

Durumun Ağaçtaki Yeri

Durumun hangi bileşende tutulacağı bir tasarım kararıdır ve doğrudan ölçülebilir bir maliyeti vardır. Bir yuva değiştiğinde o bileşen ve altındaki bütün ağaç yeniden oluşturulur; kardeşler ve atalar etkilenmez.

// durumun-yeri.mjs — ayni durumun iki ayri dugumde tutulmasinin olusturma maliyeti
const AGAC = {
  ad: "sayfa",
  cocuklar: [
    { ad: "baslik", cocuklar: [] },
    { ad: "suzgec-paneli", cocuklar: [{ ad: "suzgec-dugmesi", cocuklar: [] },
                                      { ad: "suzgec-listesi", cocuklar: [] }] },
    { ad: "olcum-tablosu", cocuklar: [{ ad: "satir-1", cocuklar: [] },
                                      { ad: "satir-2", cocuklar: [] },
                                      { ad: "satir-3", cocuklar: [] }] },
  ],
};

function bul(dugum, ad) {
  if (dugum.ad === ad) return dugum;
  for (const c of dugum.cocuklar) { const s = bul(c, ad); if (s) return s; }
  return null;
}

// Durumu tutan dugum ve butun alt agaci yeniden olusturulur.
function altAgac(dugum, toplanan = []) {
  toplanan.push(dugum.ad);
  for (const c of dugum.cocuklar) altAgac(c, toplanan);
  return toplanan;
}

const toplam = altAgac(AGAC).length;
for (const sahip of ["sayfa", "suzgec-paneli"]) {
  const cizilen = altAgac(bul(AGAC, sahip));
  console.log(`durum "${sahip}" dugumunde:`);
  console.log(`  yeniden olusturulan ${cizilen.length}/${toplam}: ${cizilen.join(", ")}`);
}
durum "sayfa" dugumunde:
  yeniden olusturulan 9/9: sayfa, baslik, suzgec-paneli, suzgec-dugmesi, suzgec-listesi, olcum-tablosu, satir-1, satir-2, satir-3
durum "suzgec-paneli" dugumunde:
  yeniden olusturulan 3/9: suzgec-paneli, suzgec-dugmesi, suzgec-listesi

Süzgeç panelinin açık olup olmadığı yalnız paneli ilgilendirir. Bu değer sayfa kökünde tutulursa her açma kapama dokuz bileşeni yeniden çizer; panelde tutulursa üçünü. Durumu onu kullanan en alt bileşende tutmaya durumun yerelde tutulması (state colocation) denir ve varsayılan tercih budur.

Ters yön de gereklidir. Seçili ölçüm türünü hem süzgeç paneli hem ölçüm tablosu bilmek zorundadır. Aynı değerin iki kopyası iki ayrı yuvada tutulursa ikisi kaçınılmaz olarak ayrışır. Böyle bir değer, iki bileşenin ortak atasında tutulur ve aşağıya verilir; buna Bileşen Kavramı dersinde durumun yukarı taşınması dendi. Ölçütü tek cümleyle yazmak olanaklıdır: durum, ona ihtiyaç duyan bütün bileşenlerin en yakın ortak atasında durur — ne daha yukarıda, ne daha aşağıda.

Başlangıç Değeri ve Tembel Kurulum

Modeldeki kanca başlangıç değerini yalnız yuva ilk açılırken kullanır; sonraki oluşturmalarda o değer hiç okunmaz. Bunun bir sonucu vardır: başlangıç değeri pahalı bir hesaptan geliyorsa — yerel depolamadan okuma, uzun bir listeyi ayrıştırma — hesap her oluşturmada boşuna yapılır. Kanca değer yerine bir işlev de kabul ederse hesap yalnız ilk oluşturmada çalışır. Buna tembel kurulum (lazy initialization) denir.

İkinci sonuç daha sık şaşırtır: üst bileşenden gelen bir değer başlangıç değeri olarak verildiğinde, o değerin sonradan değişmesi yuvayı güncellemez. Yuva ilk oluşturmada dondurulmuştur. Üstten gelen değeri izlemesi gereken bir görünüm için o değer durum olmamalı, doğrudan kullanılmalıdır.

Özet

  • Yerel durum, bileşen gövdesinde değil bileşen örneğindeki bir yuvada yaşar; gövde her oluşturmada yuvaları çağrı sırasına göre okur.
  • Yazma işlevi atama yapmaz, kuyruğa bir güncelleme isteği koyar; bir oluşturma boyunca okunan değer sabit kalır.
  • Bir turdaki bütün güncellemeler toplu işlenir ve tek oluşturma üretir; ayrı turlardaki güncellemeler ayrı oluşturmalar üretir.
  • Yeni değer eski değere bağlıysa güncelleyici işlev yazılır; doğrudan değer yazmak aynı turdaki ikinci güncellemede eskimiş değer üretir.
  • Durum kancaları koşulsuz ve döngüsüz çağrılır, çünkü yuvalar ada göre değil sıraya göre bulunur.
  • Durum, onu kullanan bileşenlerin en yakın ortak atasında tutulur; daha yukarıda tutmak gereksiz oluşturma, iki yerde tutmak ayrışma üretir.

Sonraki Adım

Bu dersin kurduğu döngü kendi içine kapalıdır: durum değişir, gövde yeniden çalışır, ağaç uzlaşır. Gövdenin bu döngüde tek bir görevi vardır ve o da girdilerden bir çıktı hesaplamaktır. Oysa ölçüm istasyonu sayfasının yapması gereken işlerin bir bölümü bu hesabın dışındadır: sunucudan ölçüm çekmek, kaydırma olayına dinleyici bağlamak, seçili süzgeci yerel depolamaya yazmak, bir zamanlayıcı kurmak. Bunlar gövdenin içine yazılamaz — gövde bir çıktı hesaplar, dış dünyaya dokunmaz. Sonraki ders bu işlerin nereye yazıldığını, ne zaman çalıştığını ve kurdukları şeyi ne zaman sökmek gerektiğini ele alır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat