İçeriğe geç
academia.sh

Ders 02 / 24

Olay Modeli

Bir olayın kökten hedefe ve hedeften köke izlediği yol; yakalama, hedef ve kabarcık evreleri, dinleyici sırası, yayılımı durdurmanın iki biçimi ve dinleyici kimliği.

İçindekiler

Önceki ders belgeyi okumayı ve değiştirmeyi kurdu. Değişikliği neyin tetikleyeceği açık kalmıştı: bir sayfa kendiliğinden değişmez, kullanıcının ya da tarayıcının ürettiği bir bildirime yanıt olarak değişir. Bu bildirime olay (event) denir.

Olayın hangi düğüme ait olduğu ilk bakışta bellidir — tıklanan düğme, yazılan alan. Asıl soru şudur: olay yalnızca o düğüme mi bildirilir? Yanıt hayırdır ve bu dersin konusu, olayın ağaç üzerinde izlediği tanımlı yoldur.

Olay Bir Nesnedir

Bir olay, tarayıcının ürettiği ve dinleyiciye argüman olarak verdiği bir nesnedir. Üç alanı her olayda bulunur.

Tür (type) olayın adıdır: click, input, submit, keydown. Dinleyici bir türe kaydolur; başka türdeki olaylar onu çağırmaz.

Hedef (target) olayın kaynaklandığı düğümdür. Tıklama olayında bu, imlecin altında kalan en derin ögedir — satırın kendisine tıklanmış gibi görünse bile, hedef o satırdaki düğmedir.

Geçerli hedef (currentTarget) dinleyicinin bağlı olduğu düğümdür. Bu ikisi yalnızca dinleyici hedefin kendisine bağlandığında aynıdır; başka her durumda ayrışırlar ve bu ayrım sonraki dersin tamamının dayanağıdır.

Dinleyicinin ne zaman çalıştığı, Eşzamansız JavaScript ve Çalışma Zamanı kursundaki Olay Döngüsü dersinde tanımlanan modele bağlıdır: olay üretildiğinde dinleyici hemen çağrılmaz, bir görev olarak kuyruğa alınır ve çağrı yığıtı boşaldığında çalıştırılır. Uzun süren bir dinleyici bu yüzden yalnızca kendini geciktirmez, kuyruktaki bütün olayları geciktirir.

Yayılım Yolu

Bir olay üretildiğinde tarayıcı önce yayılım yolunu (propagation path) hesaplar: kök düğümden hedefe kadar uzanan ata zinciri. Bu yol olay yayılmaya başlamadan önce belirlenir ve yayılım sırasında ağaç değişse bile değişmez.

Yol boyunca her düğüm iki kez ziyaret edilir: bir kez kökten hedefe inerken, bir kez hedeften köke çıkarken. Bu iki geçiş üç evre üretir.

Yakalama evresi (capture phase) kökten hedefin ebeveynine kadar iner. Atalara bağlı ve yakalama kipinde kaydedilmiş dinleyiciler burada çağrılır.

Hedef evresi (target phase) hedefin kendisidir.

Kabarcık evresi (bubble phase) hedefin ebeveyninden köke çıkar. Atalara bağlı ve kabarcık kipinde kaydedilmiş dinleyiciler burada çağrılır.

// yayilim.mjs — uc evreli olay yayiliminin kucuk bir modeli
const oge = (ad, ...cocuklar) => {
  const d = { ad, cocuklar, ebeveyn: null, dinleyiciler: [] };
  for (const c of cocuklar) c.ebeveyn = d;
  return d;
};

const dinle = (dugum, tur, etiket, yakalamada = false) =>
  dugum.dinleyiciler.push({ tur, etiket, yakalamada });

// Hedeften koke yurunur, ters cevrilir: yol her zaman kokten hedefe dogrudur.
function yol(hedef) {
  const zincir = [];
  for (let d = hedef; d; d = d.ebeveyn) zincir.push(d);
  return zincir.reverse();
}

const EVRE_ADI = { 1: "yakalama", 2: "hedef", 3: "kabarcık" };

function yay(hedef, tur) {
  const zincir = yol(hedef);
  const cagrilar = [];

  const calistir = (dugum, evre, yakalamada) => {
    for (const d of dugum.dinleyiciler) {
      if (d.tur !== tur || d.yakalamada !== yakalamada) continue;
      cagrilar.push({ evre, dugum: dugum.ad, dinleyici: d.etiket });
    }
  };

  for (const dugum of zincir.slice(0, -1)) calistir(dugum, 1, true);   // yakalama
  calistir(hedef, 2, true);                                            // hedef
  calistir(hedef, 2, false);                                           // hedef
  for (const dugum of zincir.slice(0, -1).reverse()) calistir(dugum, 3, false); // kabarcık

  return cagrilar;
}

const dugme = oge("button.kaydet");
const satir = oge("tr.olcum", dugme);
const tablo = oge("table#gunluk", satir);
const ana = oge("main", tablo);
const govde = oge("body", ana);

dinle(govde, "click", "govde-yakalama", true);
dinle(ana, "click", "ana-yakalama", true);
dinle(tablo, "click", "tablo-kabarcik");
dinle(satir, "click", "satir-yakalama", true);
dinle(satir, "click", "satir-kabarcik");
dinle(dugme, "click", "dugme-1");
dinle(dugme, "click", "dugme-2", true);

console.log("yol:", yol(dugme).map((d) => d.ad).join(" > "));
console.log();
for (const c of yay(dugme, "click")) {
  console.log(String(c.evre).padEnd(2), EVRE_ADI[c.evre].padEnd(9), c.dugum.padEnd(14), c.dinleyici);
}
yol: body > main > table#gunluk > tr.olcum > button.kaydet

1  yakalama  body           govde-yakalama
1  yakalama  main           ana-yakalama
1  yakalama  tr.olcum       satir-yakalama
2  hedef     button.kaydet  dugme-2
2  hedef     button.kaydet  dugme-1
3  kabarcık  tr.olcum       satir-kabarcik
3  kabarcık  table#gunluk   tablo-kabarcik

Çıktı, istasyon sayfasındaki ölçüm tablosunda bir satırın kaydet düğmesine tıklandığında ne olduğunu gösterir. Yedi dinleyici kayıtlıdır ve yedisi de çağrılır; çağrılma sırası kayıt sırasına değil, düğümün yoldaki konumuna ve dinleyicinin evresine bağlıdır.

table#gunluk düğümüne bağlı tablo-kabarcik dinleyicisi kayıt sırasında üçüncüdür ama çağrı sırasında sonuncudur. Bunun nedeni, kabarcık evresinin yolu ters yönde dolaşmasıdır: hedefe en yakın ata önce, en uzak ata en sonra çağrılır.

Hedefteki Sıra

Çıktının dördüncü ve beşinci satırı bir ayrıntı taşır: dugme-2 kayıtta sonuncu olmasına karşın dugme-1 den önce çağrılmıştır. Aradaki tek fark, dugme-2 nin yakalama kipinde kaydedilmiş olmasıdır.

Nedeni yolun iki kez dolaşılmasıdır. Hedef düğüm her iki geçişte de ziyaret edilir: inen geçişte yakalama kipindeki dinleyicileri, çıkan geçişte kabarcık kipindekiler çağrılır. Evre adı ikisinde de “hedef” olarak bildirilir; sıra ise geçişten gelir.

Bu davranış, aynı düğüme hem yakalama hem kabarcık kipinde dinleyici bağlanan durumlarda şaşırtıcı olabilir. Kural sadedir: aynı düğümde yakalama kipindeki dinleyiciler her zaman önce çalışır, kayıt sırasından bağımsız olarak. Aynı kipteki dinleyiciler ise kendi aralarında kayıt sırasını korur.

Yayılımı Durdurmak

Bir dinleyici, olayın yoldaki yolculuğunu kesebilir. İki ayrı işlem vardır ve etkileri farklıdır.

// durdurma.mjs — yayilimi durdurmanin iki bicimi ve kabarmayan olay
const oge = (ad, ...cocuklar) => {
  const d = { ad, cocuklar, ebeveyn: null, dinleyiciler: [] };
  for (const c of cocuklar) c.ebeveyn = d;
  return d;
};
const dinle = (dugum, etiket, secenek = {}) =>
  dugum.dinleyiciler.push({ etiket, yakalamada: secenek.yakalamada ?? false, eylem: secenek.eylem });

function yol(hedef) {
  const zincir = [];
  for (let d = hedef; d; d = d.ebeveyn) zincir.push(d);
  return zincir.reverse();
}

function yay(hedef, { kabarir = true } = {}) {
  const zincir = yol(hedef);
  const olay = { durduruldu: false, hemenDurduruldu: false, cagrilanlar: [] };

  const calistir = (dugum, yakalamada) => {
    for (const d of dugum.dinleyiciler) {
      if (d.yakalamada !== yakalamada) continue;
      if (olay.hemenDurduruldu) return;      // ayni dugumdeki kalanlari da atlar
      olay.cagrilanlar.push(dugum.ad + "/" + d.etiket);
      d.eylem?.(olay);
    }
  };

  for (const d of zincir.slice(0, -1)) {
    if (olay.durduruldu) break;              // sonraki dugume gecilmez
    calistir(d, true);
  }
  if (!olay.durduruldu) calistir(hedef, true);
  if (!olay.durduruldu) calistir(hedef, false);
  if (kabarir) {
    for (const d of zincir.slice(0, -1).reverse()) {
      if (olay.durduruldu) break;
      calistir(d, false);
    }
  }
  return olay.cagrilanlar;
}

function agacKur(satirEylem) {
  const dugme = oge("button");
  const satir = oge("tr", dugme);
  const tablo = oge("table", satir);
  const govde = oge("body", tablo);
  dinle(govde, "govde-yakalama", { yakalamada: true });
  dinle(satir, "satir-yakalama", { yakalamada: true, eylem: satirEylem });
  dinle(dugme, "dugme-1");
  dinle(dugme, "dugme-2");
  dinle(satir, "satir-kabarcik");
  dinle(tablo, "tablo-kabarcik");
  return dugme;
}

console.log("engelsiz          :", yay(agacKur(undefined)).join(" → "));
console.log("stopPropagation   :", yay(agacKur((o) => { o.durduruldu = true; })).join(" → "));
console.log("kabarmayan olay   :", yay(agacKur(undefined), { kabarir: false }).join(" → "));

// stopImmediatePropagation ayni dugumdeki sonraki dinleyiciyi de engeller.
const dugme = oge("button");
const govde2 = oge("body", dugme);
dinle(govde2, "govde-kabarcik");
dinle(dugme, "dugme-1", { eylem: (o) => { o.hemenDurduruldu = true; o.durduruldu = true; } });
dinle(dugme, "dugme-2");
console.log("stopImmediate...  :", yay(dugme).join(" → "));
engelsiz          : body/govde-yakalama → tr/satir-yakalama → button/dugme-1 → button/dugme-2 → tr/satir-kabarcik → table/tablo-kabarcik
stopPropagation   : body/govde-yakalama → tr/satir-yakalama
kabarmayan olay   : body/govde-yakalama → tr/satir-yakalama → button/dugme-1 → button/dugme-2
stopImmediate...  : button/dugme-1

stopPropagation, olayın sonraki düğüme geçmesini engeller. Aynı düğüme bağlı diğer dinleyiciler yine de çağrılır. İkinci satırda yakalama evresinde satırda durdurulan olay hedefe hiç ulaşmamıştır — düğmenin kendi dinleyicileri çalışmamıştır.

stopImmediatePropagation buna ek olarak aynı düğümdeki kalan dinleyicileri de atlar. Son satırda dugme-2 çağrılmamıştır.

İkisi arasındaki seçim bir tasarım kararıdır. stopPropagation çağıran kod, ağacın yukarısında ne olduğunu bilmeden onu iptal eder. Sayfanın başka bir yerinde belgenin tamamına bağlanmış bir dinleyici — açık bir menüyü kapatan ya da bir ölçümü kaydeden — sessizce devre dışı kalır. Bu nedenle yayılımı durdurmak, iki kod parçasının birbirini bilmeden etkilediği bir bağımlılık üretir ve son çare olarak kullanılır.

Kabaran ve Kabarmayan Olaylar

Her olay kabarmaz. Çıktının üçüncü satırı bunu gösterir: kabarmayan bir olayda yakalama ve hedef evreleri olduğu gibi çalışır, yalnızca kabarcık evresi atlanır.

Bu ayrım sık yanlış anlaşılır. Kabarmayan bir olay “yalnızca hedefe ulaşır” değildir; atalarına yakalama kipinde bağlanmış dinleyiciler onu görür. Yalnızca kabarcık kipindeki ata dinleyicileri görmez.

Kabarmayan olayların örnekleri belirli bir mantığa uyar: bir ögeye özgü olan ve ata düğümler için anlamı olmayan bildirimler kabarmaz. Odak alma ve odak kaybetme olayları kabarmaz; aynı bilgiyi kabaran bir çiftle veren ayrı olay türleri tanımlıdır. Bir görselin yüklenmesi kabarmaz. Kullanıcı girdisinden doğan olaylar — tıklama, tuş basımı, girdi, gönderim — kabarır.

Bir olayın kabarıp kabarmadığı olay nesnesinin bubbles alanından okunabilir; bu, belgeye bakarak tahmin etmekten güvenilir bir yoldur.

Dinleyici Kimliği

Bir dinleyicinin kaydı üç bilgiyle tanımlanır: olay türü, işlev başvurusu ve yakalama kipi. Aynı üçlüyle ikinci kez kayıt yapmak yeni bir dinleyici eklemez, yok sayılır. Buna karşılık aynı işlev farklı kiple kaydedilirse iki ayrı dinleyici olur.

Kaldırma da aynı üçlüyle yapılır. Buradan yaygın bir hata çıkar: kayıt sırasında yerinde tanımlanmış bir işlev, kaldırma sırasında tekrar yazıldığında başka bir işlevdir ve eşleşme kurulamaz. Kaldırılabilir olması gereken dinleyicinin işlevi bir ada bağlanmalıdır.

Kaydın seçenekleri arasında iki tanesi bu sorunu kaynağında çözer. once seçeneği, dinleyiciyi ilk çağrıdan sonra kendiliğinden kaldırır. signal seçeneği, Eşzamansız JavaScript ve Çalışma Zamanı kursundaki İptal ve Zaman Aşımı dersinde tanımlanan iptal işaretini kabul eder; işaret iptal edildiğinde o işarete bağlı bütün dinleyiciler tek hamlede kaldırılır. Çok sayıda dinleyici kaydeden bir bileşenin temizliği bu yolla tek satıra iner.

Özet

  • Olay üretildiğinde önce yayılım yolu hesaplanır: kökten hedefe uzanan ata zinciri. Yol yayılım başlamadan sabitlenir.
  • Yol iki kez dolaşılır ve üç evre üretir: yakalama (kökten hedefe), hedef, kabarcık (hedeften köke).
  • Aynı düğümde yakalama kipindeki dinleyiciler kabarcık kipindekilerden önce çalışır; aynı kip içinde kayıt sırası korunur.
  • stopPropagation olayın sonraki düğüme geçmesini, stopImmediatePropagation buna ek olarak aynı düğümdeki kalan dinleyicileri de engeller.
  • Kabarmayan olaylarda yalnızca kabarcık evresi atlanır; yakalama ve hedef evreleri değişmez.
  • Dinleyici kimliği tür, işlev başvurusu ve yakalama kipi üçlüsüdür; kaldırma aynı üçlüyü gerektirir, once ve signal seçenekleri bu gereği ortadan kaldırır.

Sonraki Adım

Bu ders olayın hedefe ulaşmadan ve ulaştıktan sonra atalarından geçtiğini gösterdi. Bu yolculuk bir yük değil, bir olanaktır: hedefin atasına bağlanmış tek bir dinleyici, o atanın altındaki bütün ögelerin olaylarını görebilir. Ölçüm tablosunun her satırına ayrı dinleyici bağlamak yerine tabloya tek dinleyici bağlamak, hem kayıt sayısını hem de sonradan eklenen satırların dinleyicisiz kalması sorununu ortadan kaldırır. Sonraki ders bu tekniği, hedefi ata düğümden ayırt etme kuralıyla birlikte kurar.

İ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