İçeriğe geç
academia.sh

Ders 03 / 24

Olay Devri

Ata düğüme bağlanan tek dinleyiciyle çok sayıda ögenin olaylarını karşılamak; hedefi geçerli hedeften ayırt etme, en yakın eşleşen atayı bulma, kimliği işaretlemede taşıma ve devrin sınırları.

İçindekiler

Önceki ders olayın hedefe ulaşmadan önce ve ulaştıktan sonra atalarından geçtiğini gösterdi. Bu yolculuk bir yük değil bir olanaktır: bir atanın dinleyicisi, o atanın altındaki bütün ögelerin olaylarını görür. Ölçüm tablosunun her satırına ayrı dinleyici bağlamak yerine tabloya tek dinleyici bağlanabilir.

Bu tekniğin adı olay devri (event delegation) dir. Devrin işe yaraması tek bir ayrıma dayanır: dinleyici ata düğüme bağlıdır, ama olayı üreten öge hedeftir. Dinleyici, kendisine gelen olayın hangi alt ögeden geldiğini çözümlemek zorundadır.

İki Sorun

Doğrudan bağlama iki ayrı sorun üretir ve ikincisi daha sinsidir.

Birincisi kayıt sayısıdır. Yüz satırlık bir tabloda satır başına bir dinleyici, yüz kayıt demektir. Her kayıt bellekte bir giriş tutar ve her kayıt, ögenin ağaçtan çıkarılmasıyla temizlenmesi gereken bir bağ kurar.

İkincisi yaşam süresidir. Dinleyici bir ögeye kaydedildiği anda var olan ögeye bağlanır. Sayfa yüklendikten sonra eklenen bir satır, kayıt sırasında ortada olmadığı için dinleyicisiz kalır. Bu, DOM API’si dersindeki canlı koleksiyon ile durağan liste ayrımının davranış tarafındaki karşılığıdır: durağan bir anda kurulan bağ, sonradan gelen düğümleri kapsamaz.

Devir iki sorunu birden çözer. Dinleyici, satırlardan daha uzun yaşayan bir ataya — tabloya — bağlanır. Satırlar gelip gitse de tablo yerinde durur.

Hedefi Çözümlemek

Devredilen dinleyicinin ilk işi, gelen olayın ilgilendiği bir ögeden gelip gelmediğine karar vermektir. Burada iki işlem birbirine karıştırılır.

Eşleşme sınaması bir ögenin verilen seçiciye uyup uymadığını söyler; yalnız o ögeye bakar. En yakın ata araması hedeften başlar, ata zincirini yukarı yürür ve seçiciye uyan ilk düğümü döndürür.

Fark, tıklamanın nereye düştüğünde ortaya çıkar. Silme düğmesinin içinde bir simge ögesi varsa, kullanıcı düğmeye bastığında hedef düğme değil simgedir. Yalnız hedefi sınayan kod bu tıklamayı kaçırır.

// enyakin.mjs — hedeften sinira dogru yuruyen es arama
const oge = (ad, ozn = {}, ...cocuklar) => {
  const d = { ad, ozn, cocuklar, ebeveyn: null };
  for (const c of cocuklar) c.ebeveyn = d;
  return d;
};

const adlandir = (d) =>
  d.ad + (d.ozn.id ? "#" + d.ozn.id : "") + (d.ozn.class ? "." + d.ozn.class : "");

const esler = (d, secici) => {
  if (secici.startsWith(".")) return (d.ozn.class ?? "").split(/\s+/).includes(secici.slice(1));
  if (secici.startsWith("#")) return d.ozn.id === secici.slice(1);
  return d.ad === secici;
};

// Hedeften baslar, sinira kadar yukari yurur; sinir da denenir, otesi denenmez.
const enYakin = (hedef, secici, sinir) => {
  for (let d = hedef; d; d = d.ebeveyn) {
    if (esler(d, secici)) return d;
    if (d === sinir) break;
  }
  return null;
};

const simge = oge("span", { class: "simge" });
const dugme = oge("button", { class: "sil" }, simge);
const satir = oge("tr", { class: "olcum", "data-kod": "T-04" }, oge("td", {}), oge("td", {}, dugme));
const tablo = oge("table", { id: "gunluk" }, satir);

console.log("hedef         :", adlandir(simge));
console.log("gecerli hedef :", adlandir(tablo));
console.log("esler(hedef, '.sil')   :", esler(simge, ".sil"));
console.log("enYakin('.sil')        :", adlandir(enYakin(simge, ".sil", tablo)));
console.log("enYakin('tr')          :", adlandir(enYakin(simge, "tr", tablo)), enYakin(simge, "tr", tablo).ozn["data-kod"]);
console.log("enYakin('body')        :", enYakin(simge, "body", tablo));
hedef         : span.simge
gecerli hedef : table#gunluk
esler(hedef, '.sil')   : false
enYakin('.sil')        : button.sil
enYakin('tr')          : tr.olcum T-04
enYakin('body')        : null

Üçüncü satır sorunu gösterir: hedefin kendisi silme düğmesi değildir. Dördüncü satır çözümü gösterir: yukarı yürüyen arama düğmeyi bulur. Beşinci satır bir adım daha gider ve düğmenin ait olduğu satırı bulur; ölçümün kodu oradan okunur.

Son satır sınır kavramını gösterir. Arama geçerli hedefte durur. Bu sınır olmadan arama belgenin köküne kadar çıkar ve dinleyicinin kapsamı dışındaki bir düğümü döndürerek başka bir bileşenin ögesi üzerinde işlem yapılmasına yol açabilir. Tarayıcı API’sinde bu işlemlerin adı matches ve closest tir; closest sınır almadığı için sonucun geçerli hedefin altında kaldığı ayrıca sınanır.

Devri Kurmak

İki yaklaşımın davranış farkı, sonradan eklenen bir satırda görünür.

// devir.mjs — her ogeye baglama ile tek dinleyiciyle devir arasindaki fark
const oge = (ad, ozn = {}, ...cocuklar) => {
  const d = { ad, ozn, cocuklar, ebeveyn: null, dinleyiciler: [] };
  for (const c of cocuklar) c.ebeveyn = d;
  return d;
};
const esler = (d, s) =>
  s.startsWith(".") ? (d.ozn.class ?? "").split(/\s+/).includes(s.slice(1)) : d.ad === s;
const enYakin = (hedef, s, sinir) => {
  for (let d = hedef; d; d = d.ebeveyn) { if (esler(d, s)) return d; if (d === sinir) break; }
  return null;
};
// Kabarcik evresi: hedeften koke dogru her dugumun dinleyicileri cagrilir.
const tikla = (hedef) => {
  for (let d = hedef; d; d = d.ebeveyn)
    for (const dinleyici of d.dinleyiciler) dinleyici({ hedef, gecerliHedef: d });
};
const dinleyiciSayisi = (kok) =>
  kok.dinleyiciler.length + kok.cocuklar.reduce((t, c) => t + dinleyiciSayisi(c), 0);

const satirKur = (kod) =>
  oge("tr", { class: "olcum", "data-kod": kod }, oge("td", {}), oge("td", {}, oge("button", { class: "sil" })));
const tabloKur = () =>
  oge("table", { id: "gunluk" },
    oge("tr", { class: "baslik" }, oge("th", {})),
    satirKur("T-01"), satirKur("T-02"), satirKur("T-03"));

const dugmeler = (kok) =>
  [kok, ...kok.cocuklar.flatMap((c) => dugmeler(c))].filter((d) => esler(d, ".sil"));

let silinen = [];
const sil = (satir) => silinen.push(satir.ozn["data-kod"]);

// Birinci yol: her silme dugmesine ayri dinleyici.
const a = tabloKur();
for (const b of dugmeler(a)) b.dinleyiciler.push(({ gecerliHedef }) => sil(gecerliHedef.ebeveyn.ebeveyn));
console.log("dogrudan baglama, dinleyici:", dinleyiciSayisi(a));
a.cocuklar.push(Object.assign(satirKur("T-04"), { ebeveyn: a })); // sonradan gelen satir
silinen = []; tikla(dugmeler(a)[0]); console.log("  T-01 dugmesi:", silinen.length ? silinen : "islem yok");
silinen = []; tikla(dugmeler(a)[3]); console.log("  T-04 dugmesi:", silinen.length ? silinen : "islem yok");

// Ikinci yol: tabloya tek dinleyici; hedef her cagrida yeniden cozumlenir.
const b = tabloKur();
b.dinleyiciler.push(({ hedef, gecerliHedef }) => {
  const dugme = enYakin(hedef, ".sil", gecerliHedef);
  if (dugme === null) return;
  sil(enYakin(dugme, "tr", gecerliHedef));
});
console.log("devir, dinleyici          :", dinleyiciSayisi(b));
b.cocuklar.push(Object.assign(satirKur("T-04"), { ebeveyn: b }));
silinen = []; tikla(dugmeler(b)[0]); console.log("  T-01 dugmesi:", silinen.length ? silinen : "islem yok");
silinen = []; tikla(dugmeler(b)[3]); console.log("  T-04 dugmesi:", silinen.length ? silinen : "islem yok");
silinen = []; tikla(b.cocuklar[0].cocuklar[0]); console.log("  baslik hucresi:", silinen.length ? silinen : "islem yok");
dogrudan baglama, dinleyici: 3
  T-01 dugmesi: [ 'T-01' ]
  T-04 dugmesi: islem yok
devir, dinleyici          : 1
  T-01 dugmesi: [ 'T-01' ]
  T-04 dugmesi: [ 'T-04' ]
  baslik hucresi: islem yok

Doğrudan bağlamada dinleyici sayısı satır sayısıyla birlikte artar ve sonradan eklenen satır sessiz kalır. Devirde tek kayıt vardır ve sonradan eklenen satır ilk satırdan farksız çalışır. Farkın kaynağı zamanlamadır: doğrudan bağlamada eşleşme kayıt anında, devirde olay anında kurulur.

Son satır devrin ikinci gereğini gösterir. Tabloya bağlı dinleyici, tablonun altındaki her tıklamayı görür — başlık hücresini, boş bir hücreyi, satır arasındaki boşluğu. Eşleşme bulunamadığında dinleyici hiçbir şey yapmadan döner. Bu erken çıkış, devredilen her dinleyicinin ilk satırıdır.

Kimliği İşaretlemede Taşımak

Devredilen dinleyici, olayın hangi kayda ait olduğunu bilmek zorundadır. Bu bilgiyi bir dizide sıra numarasıyla tutmak kırılgandır: satırlar sıralandığında ya da süzüldüğünde sıra numarası kayar.

Sağlam yol, kimliği ögenin kendisinde taşımaktır. Web’in Temelleri ve HTML kursundaki Veri Öznitelikleri dersinde tanımlanan data- öznitelikleri bu iş için vardır: değer işaretlemede durur, öge ağaçta hareket etse de değeriyle birlikte hareket eder. Yukarıdaki örnekte satırın ölçüm kodu bu yolla okunmuştur.

Aynı öznitelikler eylem türünü de taşıyabilir. Bir satırda silme, düzenleme ve işaretleme düğmeleri varsa, her biri için ayrı bir seçici sınamak yerine düğmelerin ortak bir özniteliğinde eylem adı tutulur; dinleyici tek arama yapar ve eylemi bir eşlemeden çağırır. Bu, dinleyicinin uzunluğunu satır sayısından da eylem sayısından da bağımsız kılar.

Devrin Sınırları

Devir her olay türünde işlemez ve iki sınırı vardır.

Kabarmayan olaylar devredilemez. Önceki derste tanımlandığı gibi odak alma ve odak kaybetme olayları kabarmaz; ataya bağlı kabarcık kipindeki bir dinleyici onları görmez. Bu olayların kabaran karşılıkları ayrı türler olarak tanımlıdır ve devir onlarla kurulur. İkinci yol, dinleyiciyi ataya yakalama kipinde kaydetmektir: yakalama evresi kabarmayan olaylarda da işlediği için ata düğüm olayı görür. İki yolun seçimi, olayın hedefe ulaşmadan mı ulaştıktan sonra mı işlenmesi gerektiğine bağlıdır.

Yayılımı durduran ara dinleyiciler devri sessizce bozar. Tabloya devredilmiş bir dinleyici, satırın içindeki bir dinleyici stopPropagation çağırdığında hiç çağrılmaz. Önceki derste yayılımı durdurmanın son çare olduğu söylenmişti; devir kurulmuş bir ağaçta bu, uzaktan etki eden bir kesintiye dönüşür.

Bir üçüncü nokta sınır değil ama tasarım kararıdır: dinleyicinin bağlandığı ata ne kadar yukarıdaysa kapsamı o kadar geniştir. Belgenin tamamına bağlanan bir dinleyici sayfadaki her tıklamada çalışır. Devir, ilgilenilen ögelerin ortak en yakın atasına kurulur; tablonun olayları tabloya, formun olayları forma devredilir.

Özet

  • Olay devri, ilgilenilen ögelerin bir atasına tek dinleyici bağlayıp hedefi olay anında çözümlemektir; dinleyici sayısını öge sayısından bağımsız kılar.
  • Devir, kayıt anında var olmayan ögeleri de kapsar; eşleşme kayıt anında değil olay anında kurulduğu için sonradan eklenen düğümler dinleyicisiz kalmaz.
  • Hedef, seçiciye uyan en yakın ataya çözümlenir; yalnız hedefi sınayan kod, iç içe ögelerden gelen tıklamaları kaçırır.
  • Arama geçerli hedefte durdurulur; sınırsız arama dinleyicinin kapsamı dışındaki bir düğümü döndürebilir.
  • Kayıt kimliği data- öznitelikleriyle ögenin kendisinde taşınır; sıra numarasına dayanan eşleme sıralama ve süzme sonrasında bozulur.
  • Kabarmayan olaylar kabarcık kipiyle devredilemez; yakalama kipi ya da kabaran karşılık türü kullanılır. Ara bir dinleyicinin yayılımı durdurması devri sessizce iptal eder.

Sonraki Adım

Bu derse kadar dinleyiciler yalnızca eklediler: olay geldi, kod çalıştı, sayfaya bir davranış katıldı. Oysa tarayıcının kendi davranışları da vardır ve bunlar dinleyici olmadan da işler. Bağlantıya tıklamak gezinme başlatır, gönderim düğmesi formu gönderir, onay kutusu işaretini değiştirir, boşluk tuşu sayfayı kaydırır. Bir dinleyici bu davranışların yerine kendi davranışını koymak istediğinde, tarayıcının yapacağı işi iptal etmesi gerekir. Sonraki ders bu iptalin nasıl istendiğini, hangi olaylarda mümkün olduğunu ve yayılımı durdurmakla neden karıştırılmaması gerektiğini inceler.

İ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