İçeriğe geç
academia.sh

Ders 02 / 25

Bildirimsel Oluşturma

Görünümü durumdan üreten saf fonksiyon; tek yönlü akışın döngüsü, saflığın üç koşulu, görünüm tanımının ağaçtan farkı ve tanımın tamamını yeniden kurmanın bedeli.

İçindekiler

Önceki ders elle güncellemenin bedelini saydı: durum alanı başına birden çok güncelleme yolu, aynı metnin birden çok yerde hesaplanması ve iki kopya arasındaki kaymanın kendini onaran biçimi. Çıkan sonuç, arayüzün nasıl değiştirileceğinin değil ne olması gerektiğinin yazılmasıydı.

Bu ders o yazımı bir işlev olarak kurar. İşlevin uyması gereken koşullar katıdır: aynı durum için her zaman aynı sonucu vermeli, belge ağacının o anki hâline hiç bakmamalı ve çalışırken hiçbir şeyi değiştirmemelidir. Bu üç koşul sağlandığında arayüz, durumun bir sonucuna dönüşür.

Görünüm Bir İşlevdir

Bildirimsel model tek bir denklemle özetlenir: görünüm, durumun bir işlevidir. Girdi uygulamanın o anki durumudur, çıktı arayüzün o durumdaki tam betimidir.

Çıktının belge ağacı olmadığı önemlidir. İşlev görünüm tanımı üretir: düğüm adları, öznitelikler ve çocuklardan oluşan sade bir veri yapısı. Tanım hiçbir şey çizmez, hiçbir şeye bağlı değildir ve okunabilir, karşılaştırılabilir, kopyalanabilir.

// gorunum-islevi.mjs — durumdan gorunum tanimi ureten saf islev ve tek yonlu akis
const oge = (ad, ozn = {}, ...cocuklar) => ({ ad, ozn, cocuklar: cocuklar.flat() });

// Gorunum tanimi: dugumlerden olusan sade bir veri yapisi, agac degil.
function gorunum(durum) {
  const gorunen = durum.suzgec === "tumu"
    ? durum.olcumler
    : durum.olcumler.filter((o) => o.tur === durum.suzgec);
  return oge("section", { class: "pano" },
    oge("h2", {}, `Kuzey Yamaç — ${gorunen.length} / ${durum.olcumler.length} ölçüm`),
    oge("div", { class: "suzgec" },
      ["tumu", "sicaklik", "nem"].map((s) =>
        oge("button", { deger: s, secili: s === durum.suzgec }, s))),
    oge("ul", { class: "tablo" },
      gorunen.map((o) =>
        oge("li", { class: o.deger >= o.esik ? "rozet asim" : "rozet" },
          `${o.ad}: ${o.deger} ${o.birim}`))));
}

// Olaydan yeni durum ureten saf islev: eskisini degistirmez, yenisini dondurur.
function sonrakiDurum(durum, olay) {
  if (olay.tur === "suzgecSecildi") return { ...durum, suzgec: olay.deger };
  if (olay.tur === "olcumGeldi")
    return { ...durum, olcumler: [...durum.olcumler, olay.olcum] };
  return durum;
}

const BASLANGIC = {
  suzgec: "tumu",
  olcumler: [
    { ad: "Sıcaklık", tur: "sicaklik", deger: -4.2, birim: "°C", esik: 30 },
    { ad: "Bağıl nem", tur: "nem", deger: 72, birim: "%", esik: 90 },
  ],
};
const OLAYLAR = [
  { tur: "olcumGeldi", olcum: { ad: "Zemin ısısı", tur: "sicaklik", deger: 1.8, birim: "°C", esik: 30 } },
  { tur: "suzgecSecildi", deger: "sicaklik" },
  { tur: "olcumGeldi", olcum: { ad: "Çiy noktası", tur: "nem", deger: 95, birim: "%", esik: 90 } },
];

// Akisi bastan sona calistirir; her adimda durumu ve gorunum tanimini dondurur.
function akis(baslangic, olaylar) {
  let durum = baslangic;
  const adimlar = [{ durum, tanim: gorunum(durum) }];
  for (const olay of olaylar) {
    durum = sonrakiDurum(durum, olay);
    adimlar.push({ durum, tanim: gorunum(durum) });
  }
  return adimlar;
}

const yaz = (d, girinti = 0) => typeof d === "string"
  ? `${" ".repeat(girinti)}"${d}"`
  : [`${" ".repeat(girinti)}<${d.ad}${Object.entries(d.ozn)
      .map(([k, v]) => ` ${k}=${JSON.stringify(v)}`).join("")}>`,
     ...d.cocuklar.map((c) => yaz(c, girinti + 2))].join("\n");

const birinci = akis(BASLANGIC, OLAYLAR);
console.log(yaz(birinci.at(-1).tanim));

// Ayni baslangic ve ayni olay dizisi: uretilen tanim birebir ayni mi?
const ikinci = akis(BASLANGIC, OLAYLAR);
const esitMi = birinci.every((a, i) =>
  JSON.stringify(a.tanim) === JSON.stringify(ikinci[i].tanim));
console.log(`\nayni olay dizisi yeniden calistirildi → tanimlar ayni mi: ${esitMi}`);
console.log(`baslangic durumundaki olcum sayisi (uc olaydan sonra): ` +
  `${BASLANGIC.olcumler.length}`);
console.log(`adim basina gorunen olcum: ` +
  birinci.map((a) => a.tanim.cocuklar[2].cocuklar.length).join(", "));
<section class="pano">
  <h2>
    "Kuzey Yamaç — 2 / 4 ölçüm"
  <div class="suzgec">
    <button deger="tumu" secili=false>
      "tumu"
    <button deger="sicaklik" secili=true>
      "sicaklik"
    <button deger="nem" secili=false>
      "nem"
  <ul class="tablo">
    <li class="rozet">
      "Sıcaklık: -4.2 °C"
    <li class="rozet">
      "Zemin ısısı: 1.8 °C"

ayni olay dizisi yeniden calistirildi → tanimlar ayni mi: true
baslangic durumundaki olcum sayisi (uc olaydan sonra): 2
adim basina gorunen olcum: 2, 3, 2, 2

Üç sonuç okunmalıdır. Aynı olay dizisi yeniden çalıştırıldığında üretilen tanımlar birebir aynıdır; bu, bir hatanın olay dizisi kaydedilerek yeniden üretilebileceği anlamına gelir. Başlangıç durumu üç olaydan sonra da iki ölçümde kalmıştır; durum değiştirilmemiş, her adımda yenisi üretilmiştir. Görünen ölçüm sayısı adım adım değişmiştir, ama bu sayıyı hiçbir yerde kimse güncellememiştir — süzgeç seçiminin ve ölçüm listesinin bir sonucudur.

Tek Yönlü Akış

Bu düzenin adı tek yönlü veri akışı (one-way data flow) dır. Döngü dört adımdır: durum görünüm tanımını üretir, tanım ağaca uygulanır, kullanıcı etkileşimi bir olay üretir, olay yeni bir durum üretir. Ok yönü hiçbir adımda tersine dönmez.

Tersine dönen yol iki yönlü bağlama (two-way binding) dır: arayüz parçası, okuduğu değere doğrudan yazabilir. Yazım kısalır, ama iki sonuç doğar. Birincisi, değerin nereden geldiği tek bir yere bakılarak anlaşılamaz; bir alanın değeri hem durumdan hem de kendi üzerinden gelebilir. İkincisi, birbirini besleyen iki bağ arasında sonlanma sorusu doğar: A, B’yi günceller, B, A’yı günceller. Tek yönlü akışta bu soru sorulmaz, çünkü durumu değiştiren tek bir kapı vardır.

Tek yönlü akışın bedeli, form alanı gibi doğal olarak kendi değerini taşıyan ögelerde görülür. Alanın değeri durumdan gelir, kullanıcının yazdığı harf bir olaya dönüşür, olay yeni durumu üretir ve alan yeni değeri durumdan alır. Yol uzundur; kazancı, alanın değerini durumdan başka hiçbir şeyin belirlememesidir.

Saflığın Üç Koşulu

Görünüm işlevi, Programlama Temelleri kursunda saf fonksiyon (pure function) adıyla tanımlanan koşulu sağlamalıdır; bu kursta karşılığı saf fonksiyondur. Koşul, arayüz bağlamında üç somut yasak demektir.

Aynı girdi aynı çıktı. İşlevin içinde geçerli saati okumak, rastgele sayı üretmek ya da bir sayaç artırmak, aynı durumun iki farklı tanım üretmesine yol açar. Bu değerlere gerçekten gerek varsa, işlevin içinde üretilmez; durumun bir alanı hâline getirilir ve dışarıda hesaplanır. “Son ölçüm iki dakika önce” metni, işlevin içinde okunan saatle değil, durumdaki bir zaman damgasıyla hesaplanır.

Ağaca bakmama. İşlev, o anki belge ağacından bir değer okuyup kararını ona dayandıramaz. Ağaçtan okumak, önceki dersteki iki kopya sorununu geri getirir: işlev kendi ürettiği çıktıyı girdi olarak kullanmış olur. Ölçülen bir değere gerek varsa — bir kutunun genişliği gibi — bu değer ölçülür, duruma yazılır ve bir sonraki oluşturmada girdi olarak kullanılır.

Yan etkisizlik. İşlev çalışırken ağ isteği başlatmaz, depolamaya yazmaz, zamanlayıcı kurmaz. Nedeni yalnızca temizlik değildir: bir görünüm tanımı üretildikten sonra kullanılmayabilir, aynı durum için birden çok kez üretilebilir ya da başka bir sırayla üretilebilir. Yan etki taşıyan bir işlev bu durumlarda öngörülemez davranır.

Tanım Ağaç Değildir

Tanımın ucuz, ağacın pahalı olması bu modelin çalışmasının nedenidir.

Görünüm tanımı, birkaç nesne ve dizidir; üretilmesi bellek ayırmaktan ibarettir. Belge ağacındaki bir düğüm ise tarayıcının biçem çözümlemesine, yerleşim hesabına ve boyamaya giren bir kayıttır. Bu yüzden tanım her durum değişiminde yeniden üretilebilir, ama ağaç yeniden kurulamaz.

// tam-yenileme.mjs — tanimin tamamini yeniden kurmanin bedeli
const oge = (ad, ozn = {}, ...cocuklar) => ({ ad, ozn, cocuklar: cocuklar.flat() });

function gorunum(durum) {
  return oge("section", { class: "pano" },
    oge("h2", {}, `Kuzey Yamaç — ${durum.olcumler.length} ölçüm`),
    oge("input", { ad: "arama", deger: durum.arama }),
    oge("ul", { class: "tablo" },
      durum.olcumler.map((o) =>
        oge("li", { class: "rozet" }, `${o.ad}: ${o.deger} ${o.birim}`))));
}

// Gercek agac dugumu: kimligi olan, olusturulmasi maliyetli nesne.
let uretilen = 0;
const yenidugum = (tanim) => {
  uretilen++;
  if (typeof tanim === "string") return { metin: tanim, kimlik: uretilen };
  return {
    ad: tanim.ad, ozn: { ...tanim.ozn }, kimlik: uretilen,
    cocuklar: tanim.cocuklar.map(yenidugum),
  };
};

let yazilanAlan = 0;
// En az islem: tur ayni ise dugum korunur, yalnizca degisen alan yazilir.
function uygula(dugum, tanim) {
  if (typeof tanim === "string") {
    if (!dugum || dugum.metin === undefined) return yenidugum(tanim);
    if (dugum.metin !== tanim) { dugum.metin = tanim; yazilanAlan++; }
    return dugum;
  }
  if (!dugum || dugum.ad !== tanim.ad) return yenidugum(tanim);
  for (const [k, v] of Object.entries(tanim.ozn))
    if (dugum.ozn[k] !== v) { dugum.ozn[k] = v; yazilanAlan++; }
  dugum.cocuklar = tanim.cocuklar.map((c, i) => uygula(dugum.cocuklar[i], c));
  return dugum;
}

const OLCUMLER = Array.from({ length: 12 }, (_, i) => ({
  ad: `Sensör ${String(i + 1).padStart(2, "0")}`, deger: 10 + i, birim: "°C",
}));
const ONCE = { arama: "", olcumler: OLCUMLER };
const SONRA = {
  arama: "",
  olcumler: OLCUMLER.map((o, i) => (i === 6 ? { ...o, deger: 17.5 } : o)),
};

// Ilk cizim: her iki yolda da agac sifirdan kurulur.
uretilen = 0;
const agacA = yenidugum(gorunum(ONCE));
const agacB = yenidugum(gorunum(ONCE));
const ilkMaliyet = uretilen / 2;

// Kullanici arama alanina yaziyor; bu bilgi agacta, tanimda degil.
const alanBul = (d) => d.ad === "input" ? d : d.cocuklar?.map(alanBul).find(Boolean);
alanBul(agacA).odakli = true;
alanBul(agacB).odakli = true;

// A yolu: tanimin tamami yeniden kuruluyor.
uretilen = 0;
const yeniA = yenidugum(gorunum(SONRA));
console.log(`ilk cizim                    : ${ilkMaliyet} dugum uretildi`);
console.log(`tam yeniden kurma            : ${uretilen} dugum uretildi, ` +
  `odak korundu mu: ${alanBul(yeniA).odakli === true}`);

// B yolu: yeni tanim var olan agaca uygulaniyor.
uretilen = 0; yazilanAlan = 0;
uygula(agacB, gorunum(SONRA));
console.log(`tanimi var olan agaca uygulama: ${uretilen} dugum uretildi, ` +
  `${yazilanAlan} alan yazildi, odak korundu mu: ${alanBul(agacB).odakli === true}`);
ilk cizim                    : 29 dugum uretildi
tam yeniden kurma            : 29 dugum uretildi, odak korundu mu: false
tanimi var olan agaca uygulama: 0 dugum uretildi, 1 alan yazildi, odak korundu mu: true

Tek bir ölçümün değeri değişmiştir. Ağacı baştan kurmak yirmi dokuz düğüm üretir; tanımı var olan ağaca uygulamak tek bir alan yazar ve hiç düğüm üretmez.

Maliyet farkı ikincil sonuçtur. Asıl sonuç son sütundadır: ağaçta yaşayan ve tanımda bulunmayan durum, yeniden kurmayla birlikte kaybolur. Odak bunlardan yalnızca biridir; kaydırma konumu, seçili metin aralığı, süren geçişler, medya ögesinin oynatma konumu ve gölge ağacın içindeki her şey aynı gruptadır. Görünüm tanımı bunların hiçbirini taşımaz, çünkü hiçbiri durumun parçası değildir.

Yeniden Çizim ile Uygulamanın Ayrımı

Buradan kursun geri kalanını taşıyan bir ayrım çıkar.

Yeniden çizim, görünüm işlevinin yeniden çalıştırılmasıdır. Ucuzdur ve yalnızca yeni bir tanım üretir; ekranda hiçbir şeyi değiştirmez.

Uygulama, yeni tanım ile ağacın karşılaştırılıp aradaki farkın yazılmasıdır. Pahalı olan budur ve en aza indirilmesi gereken de budur.

“Bildirimsel model her şeyi yeniden kurar” yanılgısı bu ikisini birbirine karıştırır. Yeniden çizilen şey tanımdır; ağaç yeniden kurulmaz, yamanır. Yukarıdaki uygula işlevi bu yamamanın en yalın biçimidir ve bir eksiği vardır: çocukları yalnızca sıralarına göre eşler. Listeye baştan bir kayıt eklendiğinde bütün eşleşmeler kayar. Bu eksiğin adı ve çözümü konunun son iki dersindedir.

Özet

  • Bildirimsel modelde görünüm, durumun bir işlevidir; işlevin çıktısı belge ağacı değil, sade veriden oluşan bir görünüm tanımıdır.
  • Tek yönlü akış dört adımlıdır: durum, tanım, olay, yeni durum. İki yönlü bağlama bu döngüye geri kenar ekler ve değerin kaynağını belirsizleştirir.
  • Görünüm işlevi saf olmalıdır: aynı girdiye aynı çıktı, ağaçtan okuma yok, yan etki yok. Saat ve ölçülmüş değerler işlevin içinde üretilmez, duruma yazılır.
  • Tanımın üretilmesi ucuz, ağaç düğümünün üretilmesi pahalıdır; model bu asimetri üzerine kuruludur.
  • Ağacı baştan kurmak, tanımda bulunmayan durumu — odak, kaydırma, seçim, süren geçiş — yok eder; tanımı var olan ağaca uygulamak korur.
  • Yeniden çizim tanımı üretir, uygulama farkı ağaca yazar; iki işlem ayrı maliyetlerdir.

Sonraki Adım

Bu dersin görünüm işlevi tek parçaydı: panonun tamamını tek bir işlev betimledi. On beş rozet, bir tablo ve bir süzgeç paneli aynı gövdeye sığdığında bu işlev okunamaz hâle gelir ve hiçbir parçası tek başına yeniden kullanılamaz. Sonraki ders işlevi parçalara ayırır ve her parçaya bir sözleşme verir: dışarıdan aldığı girdi, dışarıya bildirdiği olay ve yalnızca kendini ilgilendiren yerel durum.

İ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