Kısa cevap: Bir olay DOM ağacında üç evreden geçer. Önce Window düzeyinden hedefin ebeveynine kadar aşağı iner (yakalama, capturing), sonra hedefin kendisine ulaşır (hedef evresi), ardından olay kabarcıklanıyorsa aynı yolu tersine izleyerek yukarı çıkar (kabarcıklanma, bubbling). addEventListener() varsayılan olarak kabarcıklanma evresini dinler, capture: true ile yakalama evresine geçersiniz. stopPropagation() yayılmayı keser, preventDefault() ise yayılmaya dokunmadan tarayıcının varsayılan eylemini iptal eder. Olay delegasyonu da tam bu kabarcıklanmaya dayanır: Çok sayıda alt öğe yerine ortak ebeveyne tek bir dinleyici koyar, tıklananı event.target ile bulursunuz.

Bu yazıda evrelerin sırasını eventPhase değerleriyle, üç durdurma ve iptal yöntemi arasındaki farkı, target ile currentTarget ayrımını, closest() ile sağlam bir delegasyon desenini ve shadow DOM sınırında composedPath() davranışını MDN belgelerine dayanarak anlatıyoruz. Dinleyici ekleme seçenekleri için addEventListener yazısına, serinin bütün konuları için Web API öğrenme rehberi sayfasına bakabilirsiniz.

Olay akışının üç evresi

Olay akışının üç evresi: Event.NONE, Event.CAPTURING_PHASE, Event.AT_TARGET, Event.BUBBLING_PHASE

MDN Event.eventPhase sayfasında evreleri dört sabitle tanımlar. NONE (0) olayın şu anda işlenmediğini gösterir. CAPTURING_PHASE (1) sırasında olay hedefin atalarından geçer: Window, ardından Document, ardından html öğesi ve sırayla hedefin ebeveynine kadar iner; yalnızca yakalama moduyla kaydedilmiş dinleyiciler burada çalışır. AT_TARGET (2) olayın hedefe ulaştığı andır; olay kabarcıklanmıyorsa işlem bu evreyle biter. BUBBLING_PHASE (3) sırasında olay ataları ters sırayla, ebeveynden başlayıp Window nesnesine kadar dolaşır.

SabitDeğerHangi dinleyiciler çalışır
Event.NONE0Hiçbiri, olay işlenmiyor
Event.CAPTURING_PHASE1Atalardaki capture: true dinleyicileri, yukarıdan aşağıya
Event.AT_TARGET2Hedefin kendisine eklenmiş dinleyiciler
Event.BUBBLING_PHASE3Atalardaki varsayılan dinleyiciler, aşağıdan yukarıya, yalnızca bubbles true ise

Bir evre içindeki sıra da tanımlıdır: MDN'nin DOM olayları sayfasına göre aynı evrede dinleyiciler kaydedildikleri sırayla çağrılır. Hedefe doğrudan eklenmiş bir dinleyici için olay yakalama ya da kabarcıklanma evresinde değil, hedef evresindedir. Aşağıdaki küçük deneme sabitlerin değerlerini ve hedefe eklenmiş bir dinleyicinin gördüğü evreyi gösterir; standart Event ve EventTarget kullandığı için Node.js 24.4.0 ile çalıştırıp doğruladık.

javascript
const hedef = new EventTarget();
const e = new Event("deneme");
console.log(e.bubbles, e.cancelable, e.composed); // false false false
console.log(e.eventPhase); // 0
hedef.addEventListener("deneme", (ev) => console.log(ev.eventPhase)); // 2
hedef.dispatchEvent(e);
console.log(Event.CAPTURING_PHASE, Event.AT_TARGET, Event.BUBBLING_PHASE); // 1 2 3

Kendi oluşturduğunuz olaylarda bubbles varsayılan olarak false olduğuna dikkat edin. Bir alt öğeden gönderdiğiniz özel olayın ebeveynde yakalanmasını istiyorsanız olayı bubbles: true ile oluşturmanız gerekir; bu konu CustomEvent yazısında örnekleniyor.

Yakalama ve kabarcıklanmayı sayfada görmek

Aşağıdaki HTML iç içe üç kutu içerir. Her kutuya hem yakalama hem kabarcıklanma dinleyicisi eklenir ve dinleyiciler currentTarget kimliğini ve evre adını bir listeye yazar. MDN'nin eventPhase örneği aynı fikri dört katmanlı bir yapıyla gösterir; biz onu sadeleştirdik. Bu örnek tarayıcıya özgüdür ve çalıştırılmadı, beklenen sıra MDN'nin evre tanımından çıkarılmıştır.

html
<div id="dis">
  dış
  <div id="orta">
    orta
    <button type="button" id="ic">iç düğme</button>
  </div>
</div>
<ol id="kayit"></ol>
javascript
const kayit = document.getElementById("kayit");
const evreler = ["none", "capturing", "target", "bubbling"];

function yaz(event) {
  const li = document.createElement("li");
  li.textContent = `${event.currentTarget.id}: ${evreler[event.eventPhase]}`;
  kayit.append(li);
}

for (const id of ["dis", "orta", "ic"]) {
  const el = document.getElementById(id);
  el.addEventListener("click", yaz, { capture: true });
  el.addEventListener("click", yaz);
}

Düğmeye tıklandığında önce dis ve orta üzerindeki yakalama dinleyicileri capturing olarak, sonra düğmenin iki dinleyicisi target olarak, en son orta ve dis üzerindeki varsayılan dinleyiciler bubbling olarak listeye düşer. Düğmede iki dinleyici de hedef evresinde çalıştığı için aralarındaki sıra kayıt sırasıdır.

stopPropagation, stopImmediatePropagation ve preventDefault

stopPropagation, stopImmediatePropagation ve preventDefault: stopPropagation(), stopImmediatePropagation(), preventDefault()

Bu üç yöntem sık karıştırılır, çünkü üçü de olayı bir şekilde "durdurur". Aralarındaki fark neyin durduğundadır. stopPropagation() olayın yakalama ve kabarcıklanma yolunda sonraki öğelere ulaşmasını engeller; ancak MDN'nin açıkça belirttiği gibi aynı öğedeki diğer dinleyicileri durdurmaz ve bağlantı tıklaması gibi varsayılan davranışları da engellemez. stopImmediatePropagation() bir adım ileri gider: Aynı öğede henüz çalışmamış dinleyiciler de dahil olmak üzere kalan hiçbir dinleyici çağrılmaz. preventDefault() ise yayılmayla ilgilenmez; olay yoluna devam eder ama tarayıcının varsayılan eylemi, örneğin sayfa kaydırma, bağlantıya gitme ya da form gönderme gerçekleşmez.

YöntemDiğer öğelere yayılımAynı öğedeki sonraki dinleyicilerVarsayılan eylem
stopPropagation()DururÇalışırGerçekleşir
stopImmediatePropagation()DururÇalışmazGerçekleşir
preventDefault()Devam ederÇalışırİptal edilir (olay iptal edilebilirse)

Aynı hedefteki dinleyiciler arasındaki farkı tarayıcı olmadan da görebilirsiniz. Aşağıdaki örnek iki ayrı hedefte üçer ve ikişer dinleyici kaydeder. İlkinde ikinci dinleyici stopImmediatePropagation() çağırdığı için üçüncü çalışmaz; ikincisinde ise stopPropagation() aynı hedefteki ikinci dinleyiciyi engellemez. Örnek Node.js 24.4.0 ile çalıştırıldı.

javascript
const hedef = new EventTarget();
const sira = [];
hedef.addEventListener("tikla", () => sira.push("birinci"));
hedef.addEventListener("tikla", (e) => {
  sira.push("ikinci");
  e.stopImmediatePropagation();
});
hedef.addEventListener("tikla", () => sira.push("ucuncu"));
hedef.dispatchEvent(new Event("tikla"));
console.log(sira.join(", ")); // birinci, ikinci

const hedef2 = new EventTarget();
const sira2 = [];
hedef2.addEventListener("tikla", (e) => {
  sira2.push("birinci");
  e.stopPropagation();
});
hedef2.addEventListener("tikla", () => sira2.push("ikinci"));
hedef2.dispatchEvent(new Event("tikla"));
console.log(sira2.join(", ")); // birinci, ikinci

preventDefault() yalnızca iptal edilebilir olaylarda işe yarar. Tarayıcının kullanıcı etkileşiminden ürettiği click, wheel ve beforeunload gibi olaylar iptal edilebilir; betikle oluşturulan olaylar ise oluşturulurken cancelable: true verilmediyse iptal edilemez ve preventDefault() çağrısı etkisiz kalır. dispatchEvent() dönüş değeri de bunu yansıtır: Olay iptal edilebilir ve bir dinleyici preventDefault() çağırdıysa false, aksi halde true döner.

javascript
const hedef = new EventTarget();
hedef.addEventListener("kaydet", (e) => e.preventDefault());

const iptalEdilemez = new Event("kaydet");
console.log(hedef.dispatchEvent(iptalEdilemez)); // true
console.log(iptalEdilemez.defaultPrevented); // false

const iptalEdilebilir = new Event("kaydet", { cancelable: true });
console.log(hedef.dispatchEvent(iptalEdilebilir)); // false
console.log(iptalEdilebilir.defaultPrevented); // true

MDN birden çok olay türünü işleyen dinleyicilerin preventDefault() çağırmadan önce event.cancelable değerine bakmasını önerir. Pasif dinleyicilerde de preventDefault() etkisizdir; bu ayrıntı addEventListener yazısındaki passive bölümünde anlatılıyor. Eski kodlarda görebileceğiniz event.cancelBubble = true ataması stopPropagation() için tarihsel bir takma addır ve MDN onu kullanımdan kaldırılmış olarak işaretler; event.returnValue = false da benzer şekilde yerini preventDefault() ve defaultPrevented özelliğine bırakmış tarihsel bir özelliktir.

target ile currentTarget arasındaki fark

Delegasyonu anlamak için bu iki özelliğin farkı şarttır. event.target olayın ilk gönderildiği nesnedir; kullanıcı bir listedeki li içindeki span öğesine tıkladıysa target o span olur. event.currentTarget ise şu anda çalışan dinleyicinin bağlı olduğu nesnedir; dinleyiciyi ul üzerine koyduysanız currentTarget her zaman ul olur. Olay kabarcıklanırken target değişmez, currentTarget her öğede değişir. MDN ayrıca currentTarget değerinin yalnızca dinleyici çalışırken dolu olduğunu, dışarıda null olduğunu hatırlatır.

Olay delegasyonu nasıl kurulur?

Delegasyonda dinleyiciyi tek tek her alt öğeye eklemek yerine onları kapsayan ortak bir ebeveyne bir kez eklersiniz. Alt öğedeki tıklama kabarcıklanarak ebeveyne ulaşır, dinleyici de event.target üzerinden hangi öğenin tıklandığını bulur. Bu yaklaşımın iki pratik kazancı vardır: Yüzlerce satırlık bir liste için yüzlerce fonksiyon kaydı yerine tek kayıt yapılır ve listeye sonradan eklenen öğeler, ayrıca dinleyici eklemeye gerek kalmadan otomatik olarak kapsanır. MDN'nin Event.target sayfasındaki örnek tam olarak budur: Dinleyici ul üzerinde durur, tıklanan li gizlenir.

Gerçek uygulamada tıklanan öğe çoğu zaman aradığınız öğenin içindeki bir simge ya da metin parçasıdır. Bu yüzden doğrudan event.target ile karşılaştırmak yerine Element.closest() kullanmak daha sağlamdır. closest() öğenin kendisinden başlayarak belge köküne doğru ilerler ve verilen CSS seçicisine uyan ilk öğeyi döndürür; bulamazsa null verir, geçersiz bir seçici verilirse SyntaxError fırlatır. Bulunan öğenin gerçekten dinleyicinin bağlı olduğu kapsayıcının içinde olduğunu da contains() ile doğrulamak, iç içe aynı sınıfı kullanan yapılarda sürprizleri önler.

html
<ul id="gorevler">
  <li data-id="1"><span class="ad">Faturayı öde</span> <button class="sil" type="button">Sil</button></li>
  <li data-id="2"><span class="ad">Yedek al</span> <button class="sil" type="button">Sil</button></li>
</ul>
<button type="button" id="ekle">Görev ekle</button>
javascript
const liste = document.getElementById("gorevler");

liste.addEventListener("click", (event) => {
  const silDugmesi = event.target.closest("button.sil");
  if (!silDugmesi || !liste.contains(silDugmesi)) {
    return; // tıklama bir sil düğmesinden gelmedi
  }
  const satir = silDugmesi.closest("li");
  console.log("Silinen görev:", satir.dataset.id);
  satir.remove();
});

document.getElementById("ekle").addEventListener("click", () => {
  const li = document.createElement("li");
  li.dataset.id = String(liste.children.length + 1);
  li.innerHTML = '<span class="ad">Yeni görev</span> <button class="sil" type="button">Sil</button>';
  liste.append(li); // yeni satır için ayrıca dinleyici gerekmez
});

Örnekte dataset okuması yalnızca kimliği taşımak için kullanıldı; data-* öznitelikleri HTML serisinde HTML öğrenme rehberi üzerinden anlatılıyor. Yeni öğe oluşturma ve remove() gibi ağaç işlemleri için DOM eleman oluşturma yazısına, closest() ve diğer gezinme özellikleri için DOM ağacında gezinme yazısına bakabilirsiniz.

Kabarcıklanmayan olaylar

Delegasyon yalnızca kabarcıklanan olaylarla çalışır. event.bubbles özelliği bunu söyler. MDN sayfalarında her olayın kabarcıklanıp kabarcıklanmadığı yazar; örneğin klavye olayları kabarcıklanır ve Document ile Window düzeyine ulaşabilir, buna karşılık load, readystatechange ve formdata olaylarının sayfalarında kabarcıklanmadıkları belirtilir. load için ayrıca ilginç bir kural vardır: load adlı hiçbir olay, bubbles değeri true olsa bile Window nesnesine yayılmaz. Kabarcıklanmayan bir olayı ataların birinde yakalamak gerekiyorsa yakalama evresi bir seçenektir, çünkü yakalama evresi olay kabarcıklansa da kabarcıklanmasa da çalışır.

Shadow DOM sınırında composed ve composedPath

Web bileşenlerinde olayın yolu shadow DOM sınırından da geçebilir. event.composed, olayın shadow ağacından normal DOM'a geçip geçmeyeceğini gösterir. MDN'ye göre tarayıcının gönderdiği tıklama, dokunma, fare üzerine gelme, kopyalama ve yapıştırma gibi kullanıcı arayüzü olaylarının hepsi composed olarak gelir; betikle oluşturulan olaylar ise composed: true verilmedikçe sınırı geçmez. Yayılma için bubbles değerinin de true olması gerekir.

event.composedPath() dinleyicilerin çağrılacağı nesnelerin dizisini döndürür. Shadow kökü mode: "closed" ile oluşturulduysa iç düğümler bu diziye dahil edilmez. MDN örneğinde açık shadow köklü bir öğeye tıklandığında yol p, ShadowRoot, özel öğe, body, html, belge ve Window şeklinde görünürken kapalı köklü öğede yol özel öğenin kendisinden başlar. Delegasyon yaparken shadow içindeki gerçek hedefe ihtiyacınız varsa composedPath() dizisinin ilk elemanına bakabilirsiniz; kapalı köklerde bu mümkün değildir. Özel öğeler ve shadow DOM için Web Components yazısına bakın.

Sık yapılan hatalar

Sık yapılan hatalar: stopPropagation ile varsayılan eylemi durdurmayı beklemek, Her yerde stopPropagation kullanmak, event.target ile doğrudan eşitlik aramak, Özel olayda bubbles...
  • stopPropagation ile varsayılan eylemi durdurmayı beklemek: Bağlantı yine açılır, form yine gönderilir. Varsayılan eylem için preventDefault() gerekir.
  • Her yerde stopPropagation kullanmak: Yayılmayı kesmek, belge düzeyinde delegasyonla çalışan başka kodları (analiz, menü kapatma, kütüphane dinleyicileri) sessizce bozar. Önce olayı gerçekten kesmeniz gerekip gerekmediğini düşünün.
  • event.target ile doğrudan eşitlik aramak: Kullanıcı düğme içindeki bir simgeye tıklarsa target simge olur. closest() kullanın.
  • Özel olayda bubbles vermeyi unutmak: Kendi olaylarınız varsayılan olarak kabarcıklanmaz, ebeveyndeki dinleyici hiçbir şey almaz.
  • Yakalama dinleyicisini kaldırırken capture vermemek: capture: true ile eklenen dinleyici aynı değer verilmeden kaldırılamaz.
  • Kabarcıklanmayan olayı delegasyonla dinlemek: Olayın MDN sayfasında kabarcıklanıp kabarcıklanmadığını kontrol edin.

Sık Sorulan Sorular

Varsayılan olarak hangi evrede dinlerim?

Kabarcıklanma evresinde. addEventListener() çağrısında capture belirtmezseniz değer false kabul edilir. Dinleyici hedefin kendisindeyse olay hedef evresindedir.

stopPropagation sayfadaki diğer dinleyicileri de durdurur mu?

Yalnızca yayılma yolunda sonra gelecek öğelerdeki dinleyicileri durdurur. Aynı öğede kayıtlı diğer dinleyiciler çalışmaya devam eder; onları da durdurmak için stopImmediatePropagation() gerekir.

Olay delegasyonu performans için her zaman daha mı iyi?

MDN bellek bölümünde tek bir paylaşılan fonksiyonun döngüde üretilen çok sayıda anonim fonksiyondan daha az bellek kullandığını belirtir. Delegasyon kayıt sayısını bire indirir ve dinamik öğeleri kendiliğinden kapsar. Öte yandan Pointer Events belgesindeki en iyi uygulamalar, işaretçi olaylarını belgenin üst düğümleri yerine belirli hedef öğeye eklemeyi önerir; yani olay türüne göre karar verin.

preventDefault çağırdığım olay neden yine de iptal olmadı?

Olay iptal edilebilir değildir (cancelable değeri false), dinleyici pasif olarak eklenmiştir ya da olay betikle cancelable: true verilmeden oluşturulmuştur. event.cancelable ve event.defaultPrevented değerlerini kontrol ederek hangisi olduğunu görebilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · addEventListener kullanımı · CustomEvent ve EventTarget · DOM ağacında gezinme · Web Components ve shadow DOM · Form gönderme ve FormData