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.
stopPropagationolayın sonraki düğüme geçmesini,stopImmediatePropagationbuna 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,
oncevesignalseç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.