preventDefault
ve stopPropagation
: Hangi yöntemin ne zaman kullanılacağı ve her yöntemin tam olarak ne yaptığı.
Event.stopPropagation() ve Event.preventDefault()
JavaScript etkinlik işleme genellikle basittir. Bu durum özellikle basit (nispeten düz) bir HTML yapısıyla uğraşırken geçerlidir. Ancak etkinlikler bir öğe hiyerarşisinde hareket ettiğinde (veya yayıldığında) işler biraz daha karmaşık hale gelir. Geliştiriciler genellikle karşılaştıkları sorunları çözmek için stopPropagation()
ve/veya preventDefault()
kaynaklarına başvurur. Hiç "Önce preventDefault()
'yı denerim, işe yaramazsa stopPropagation()
'yı denerim, o da işe yaramazsa ikisini birden denerim" diye düşündüyseniz bu makale tam size göre! Her yöntemin tam olarak ne yaptığını, hangisinin ne zaman kullanılacağını açıklayacak ve incelemeniz için çeşitli çalışan örnekler sunacağım. Amacım, kafa karışıklığınızı tamamen gidermek.
Ancak konuya çok fazla girmeden önce, JavaScript'te (tüm modern tarayıcılarda, yani 9. sürümden önceki Internet Explorer'da etkinlik yakalama hiç desteklenmiyordu) mümkün olan iki tür etkinlik işleme konusuna kısaca değinmek önemlidir.
Olay stilleri (yakalama ve kabarcıklandırma)
Tüm modern tarayıcılar etkinlik yakalamayı destekler ancak geliştiriciler tarafından çok nadiren kullanılır.
İlginç bir şekilde, Netscape'in başlangıçta desteklediği tek etkinlik biçimi buydu. Netscape'in en büyük rakibi Microsoft Internet Explorer, etkinlik yakalamayı hiç desteklemiyor, bunun yerine yalnızca etkinlik kabarcıklanması adı verilen başka bir etkinlik stilini destekliyordu. W3C kurulduğunda her iki etkinlik stilinin de avantajlı olduğu görülmüş ve tarayıcıların addEventListener
yöntemine üçüncü bir parametre ekleyerek her ikisini de desteklemesi gerektiği belirtilmiştir. Başlangıçta bu parametre yalnızca bir boole değeriyken tüm modern tarayıcılar üçüncü parametre olarak bir options
nesnesini destekler. Bu nesneyi, etkinlik yakalama özelliğini kullanmak isteyip istemediğinizi (diğer şeylerin yanı sıra) belirtmek için kullanabilirsiniz:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
options
nesnesinin ve capture
özelliğinin isteğe bağlı olduğunu unutmayın. İkisinden biri atlanırsa capture
için varsayılan değer false
olur. Bu da etkinlik kabarcıklanmasının kullanılacağı anlamına gelir.
Etkinlik yakalama
Etkinlik işleyicinizin "yakalama aşamasında dinlemesi" ne anlama gelir? Bunu anlamak için etkinliklerin nasıl başladığını ve nasıl ilerlediğini bilmemiz gerekir. Geliştirici olarak siz bu özelliği kullanmasanız, önemsemeseniz veya düşünmeseniz bile tüm etkinlikler için aşağıdakiler geçerlidir.
Tüm etkinlikler pencerede başlar ve önce yakalama aşamasından geçer. Bu, bir etkinlik gönderildiğinde pencerenin başladığı ve öncelikle hedef öğesine doğru "aşağıya" hareket ettiği anlamına gelir. Bu durum, yalnızca bubbling aşamasında dinliyor olsanız bile geçerlidir. Aşağıdaki örnek işaretlemeyi ve JavaScript'i inceleyin:
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('#C was clicked');
},
true,
);
Kullanıcı #C
öğesini tıkladığında window
öğesinden kaynaklanan bir etkinlik gönderilir. Bu etkinlik daha sonra aşağıdaki şekilde alt öğelerine yayılır:
window
=> document
=> <html>
=> <body>
=> ve hedef değere ulaşana kadar bu şekilde devam eder.
window
veya document
ya da <html>
öğesinde veya <body>
öğesinde (ya da hedefine giderken yoldaki başka bir öğede) tıklama etkinliğini dinleyen bir şey olmaması önemli değildir. Etkinlikler hâlâ window
adresinde oluşturulur ve yolculuğuna yukarıda açıklandığı gibi başlar.
Örneğimizde, tıklama etkinliği daha sonra yayılır (Bu kelime, stopPropagation()
yönteminin nasıl çalıştığıyla doğrudan bağlantılı olduğu için önemlidir ve bu belgenin ilerleyen bölümlerinde açıklanacaktır). window
ile hedef öğesi (bu örnekte #C
) arasındaki her öğe aracılığıyla window
öğesinden hedef öğesine yayılır.#C
Bu, tıklama etkinliğinin window
tarihinde başlayacağı ve tarayıcının aşağıdaki soruları soracağı anlamına gelir:
"Yakalama aşamasında window
üzerinde tıklama etkinliğini dinleyen bir şey var mı?" Bu durumda uygun etkinlik işleyiciler tetiklenir. Örneğimizde hiçbir şey tetiklenmediği için işleyiciler de tetiklenmez.
Ardından, etkinlik document
'ye yayılır ve tarayıcı şu soruyu sorar: "Yakalama aşamasında document
üzerinde tıklama etkinliğini dinleyen bir şey var mı?" Bu durumda, uygun etkinlik işleyiciler tetiklenir.
Ardından, etkinlik <html>
öğesine yayılır ve tarayıcı şu soruyu sorar: "Yakalama aşamasında <html>
öğesinin tıklanmasını dinleyen bir şey var mı?" Bu durumda, uygun etkinlik işleyicileri tetiklenir.
Ardından, etkinlik <body>
öğesine yayılır ve tarayıcı şu soruyu sorar: "Yakalama aşamasında <body>
öğesinde tıklama etkinliğini dinleyen bir şey var mı?" Bu durumda, uygun etkinlik işleyiciler tetiklenir.
Ardından etkinlik, #A
öğesine yayılır. Tarayıcı yine şu soruyu sorar: "Yakalama aşamasında #A
üzerinde tıklama etkinliğini dinleyen bir şey var mı? Varsa uygun etkinlik işleyicileri tetiklenir.
Ardından, etkinlik #B
öğesine yayılır (ve aynı soru sorulur).
Son olarak, etkinlik hedefine ulaşır ve tarayıcı şu soruyu sorar: "Yakalama aşamasında #C
öğesinde tıklama etkinliğini dinleyen bir şey var mı?" Bu sorunun cevabı "evet!" Etkinliğin hedef üzerinde olduğu bu kısa süreye "hedef aşaması" adı verilir. Bu noktada, etkinlik işleyici tetiklenir, tarayıcı "#C was clicked" ifadesini console.log ile kaydeder ve işlem tamamlanır, değil mi?
Yanlış! Henüz işimiz bitmedi. Süreç devam eder ancak artık kabarcık oluşturma aşamasına geçer.
Etkinlik kabarcıklanması
Tarayıcı şu soruyu sorar:
"Taşma aşamasında #C
üzerinde tıklama etkinliğini dinleyen bir şey var mı?" Bu noktaya dikkat edin.
Yakalama ve taşma aşamalarında tıklamaları (veya herhangi bir etkinlik türünü) dinlemek tamamen mümkündür. Ayrıca, her iki aşamada da olay işleyicileri bağladıysanız (ör. .addEventListener()
işlevini iki kez, bir kez capture = true
ile bir kez de capture = false
ile çağırarak) aynı öğe için her iki olay işleyici de kesinlikle tetiklenir. Ancak bunların farklı aşamalarda tetiklendiğini (biri yakalama aşamasında, diğeri ise kabarcık oluşturma aşamasında) unutmamak gerekir.
Ardından etkinlik, üst öğesine (#B
) yayılır (DOM ağacında "yukarı" doğru hareket ediyormuş gibi göründüğünden daha çok "taşma" olarak ifade edilir) ve tarayıcı şu soruyu sorar: "Taşma aşamasında #B
üzerinde tıklama etkinliklerini dinleyen bir şey var mı?" Örneğimizde hiçbir şey değişmediği için işleyiciler tetiklenmez.
Ardından, etkinlik #A
'ya yayılır ve tarayıcı şu soruyu sorar: "Yayılma aşamasında #A
üzerinde tıklama etkinliklerini dinleyen bir şey var mı?"
Ardından, etkinlik <body>
'ya yayılır: "Yayılma aşamasında <body>
öğesinde tıklama etkinliklerini dinleyen bir şey var mı?"
Ardından, <html>
öğesi: "Kabarcıklanma aşamasında <html>
öğesindeki tıklama etkinliklerini dinleyen bir şey var mı?
Ardından document
: "Kabarcıklanma aşamasında document
üzerinde tıklama etkinliklerini dinleyen bir şey var mı?"
Son olarak, window
: "Kabarcıklanma aşamasında pencerede tıklama etkinliklerini dinleyen bir şey var mı?"
Bora Bu uzun bir yolculuktu ve etkinliğimiz muhtemelen çok yoruldu. Ancak inanın ya da inanmayın, her etkinlik bu yolculuktan geçer. Geliştiriciler genellikle yalnızca bir etkinlik aşamasıyla ilgilendiğinden (bu genellikle kabarcıklanma aşamasıdır) bu durum çoğu zaman fark edilmez.
Etkinlik yakalama, etkinlik kabarcıklanması ve günlük kaydıyla biraz zaman geçirip işleyiciler tetiklenirken konsola bazı notlar kaydetmek faydalı olacaktır. Bir etkinliğin izlediği yolu görmek çok faydalıdır. Aşağıda, her iki aşamadaki her öğeyi dinleyen bir örnek verilmiştir.
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.addEventListener(
'click',
function (e) {
console.log('click on document in capturing phase');
},
true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in capturing phase');
},
true,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in capturing phase');
},
true,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in capturing phase');
},
true,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in capturing phase');
},
true,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in capturing phase');
},
true,
);
document.addEventListener(
'click',
function (e) {
console.log('click on document in bubbling phase');
},
false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in bubbling phase');
},
false,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in bubbling phase');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in bubbling phase');
},
false,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in bubbling phase');
},
false,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in bubbling phase');
},
false,
);
Konsol çıkışı, hangi öğeyi tıkladığınıza bağlıdır. DOM ağacındaki "en derin" öğeyi (#C
öğesi) tıklarsanız bu etkinlik işleyicilerin her birinin tetiklendiğini görürsünüz. Hangi öğenin hangisi olduğunu daha net hale getirmek için biraz CSS stiliyle birlikte, konsol çıkışı #C
öğesini (ekran görüntüsüyle birlikte) aşağıda görebilirsiniz:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"
event.stopPropagation()
Etkinliklerin nereden kaynaklandığını ve hem yakalama hem de taşma aşamasında DOM'da nasıl hareket ettiğini (yani nasıl yayıldığını) anladığımıza göre artık event.stopPropagation()
konusuna odaklanabiliriz.
stopPropagation()
yöntemi, (çoğu) yerel DOM etkinliğinde çağrılabilir. "Çoğu" diyorum çünkü bu yöntemin çağrılmasının hiçbir işe yaramayacağı birkaç olay var (çünkü olay başlangıçta yayılmaz). focus
, blur
, load
, scroll
gibi etkinlikler ve birkaç etkinlik daha bu kategoriye girer. stopPropagation()
numarayı arayabilirsiniz ancak bu etkinlikler yayılmadığından ilginç bir şey olmaz.
Peki stopPropagation
ne işe yarar?
Bu özellik, adının hakkını veriyor. Bu işlevi çağırdığınızda etkinlik, o noktadan itibaren normalde gideceği öğelere yayılmayı durdurur. Bu durum, her iki yön için (yakalama ve kabarcık oluşturma) geçerlidir. Bu nedenle, yakalama aşamasında stopPropagation()
işlevini herhangi bir yerde çağırırsanız etkinlik hiçbir zaman hedef aşamaya veya kabarcık aşamasına ulaşmaz. Kabarcıklanma aşamasında çağırırsanız yakalama aşamasından geçmiş olur ancak çağrıldığı noktadan itibaren "kabarcıklanmayı" durdurur.
Aynı örnek işaretlememize dönecek olursak yakalama aşamasında stopPropagation()
işlevini #B
öğesinde çağırsaydık ne olurdu?
Aşağıdaki çıktı elde edilir:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
Kabarcıklanma aşamasında #A
'da yayılmayı durdurmaya ne dersiniz? Bu durumda aşağıdaki çıktı elde edilir:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
Bir tane daha, sadece eğlence için. #C
için hedef aşamasında stopPropagation()
çağrısı yaparsak ne olur?
"Hedef aşaması"nın, etkinliğin hedefinde olduğu döneme verilen ad olduğunu hatırlayın. Aşağıdaki çıktı elde edilir:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"Yakalama aşamasında #C öğesi tıklanıyor" ifadesini kaydettiğimiz #C
için etkinlik işleyicinin hâlâ yürütüldüğünü, ancak "Kabarcıklanma aşamasında #C öğesi tıklanıyor" ifadesini kaydettiğimiz işleyicinin yürütülmediğini unutmayın. Bu, mükemmel bir anlam ifade etmelidir. Önceki stopPropagation()
konumundan aradığımız için etkinliğin yayılması bu noktada durur.
Bu canlı demoların herhangi birinde denemeler yapmanızı öneririz. Yalnızca #A
öğesini veya yalnızca body
öğesini tıklamayı deneyin. Ne olacağını tahmin etmeye çalışın ve tahmininizin doğru olup olmadığını gözlemleyin. Bu noktada, oldukça doğru tahminlerde bulunabilirsiniz.
event.stopImmediatePropagation()
Bu garip ve sık kullanılmayan yöntem nedir? Bu yöntem, stopPropagation
yöntemine benzer ancak bir etkinliğin alt öğelere (yakalama) veya üst öğelere (kabarcıklanma) gitmesini durdurmak yerine yalnızca tek bir öğeye bağlı birden fazla etkinlik işleyiciniz olduğunda geçerlidir. addEventListener()
, çoklu yayın tarzı bir etkinlik sistemini desteklediğinden bir etkinlik işleyicisini tek bir öğeye birden fazla kez bağlamak tamamen mümkündür. Bu durumda (çoğu tarayıcıda) etkinlik işleyiciler, bağlandıkları sırayla yürütülür. Calling stopImmediatePropagation()
, sonraki işleyicilerin tetiklenmesini engeller. Aşağıdaki örneği inceleyelim:
<html>
<body>
<div id="A">I am the #A element</div>
</body>
</html>
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run first!');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run second!');
e.stopImmediatePropagation();
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
},
false,
);
Yukarıdaki örnek aşağıdaki konsol çıktısıyla sonuçlanır:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
İkinci etkinlik işleyicisi e.stopImmediatePropagation()
işlevini çağırdığı için üçüncü etkinlik işleyicisinin hiçbir zaman çalışmadığını unutmayın. Bunun yerine e.stopPropagation()
adlı üçüncü işleyiciyi çağırsaydık yine çalışırdı.
event.preventDefault()
stopPropagation()
bir olayın "aşağıya" (yakalama) veya "yukarıya" (kabarcık) gitmesini engelliyorsa preventDefault()
ne yapar? Benzer bir işlev sunuyor gibi görünüyor. Does
it?
Pek sayılmaz. Bu iki kavram genellikle karıştırılsa da aslında birbiriyle pek alakası yoktur.
preventDefault()
simgesini gördüğünüzde zihninizde "eylem" kelimesini ekleyin. "Varsayılan işlemi engelle"yi düşünün.
Varsayılan işlem nedir diye sorabilirsiniz. Maalesef bu sorunun cevabı, söz konusu öğe ve etkinlik kombinasyonuna büyük ölçüde bağlı olduğundan net değildir. İşleri daha da karmaşık hale getiren bir diğer durum ise bazen hiç varsayılan işlem olmamasıdır.
Anlaşılması kolay olması için çok basit bir örnekle başlayalım. Bir web sayfasındaki bağlantıyı tıkladığınızda ne olmasını beklersiniz? Tarayıcının, bağlantı tarafından belirtilen URL'ye gitmesini beklersiniz.
Bu durumda öğe bir bağlantı etiketi, etkinlik ise tıklama etkinliğidir. Bu kombinasyon (<a>
+ click
), bağlantının href'ine gitme"varsayılan işlemine" sahiptir. Tarayıcının bu varsayılan işlemi yapmasını engellemek istiyorsanız ne yapmanız gerekir? Yani, tarayıcının <a>
öğesinin href
özelliğiyle belirtilen URL'ye gitmesini engellemek istediğinizi varsayalım. preventDefault()
sizin için şunları yapar. Aşağıdaki örneğe bakın:
<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
'click',
function (e) {
e.preventDefault();
console.log('Maybe we should just play some of their music right here instead?');
},
false,
);
Normalde, The Avett Brothers etiketli bağlantının tıklanması www.theavettbrothers.com
adresine göz atılmasıyla sonuçlanır. Ancak bu örnekte, <a>
öğesine bir tıklama etkinliği işleyicisi bağladık ve varsayılan işlemin engellenmesi gerektiğini belirttik. Bu nedenle, bir kullanıcı bu bağlantıyı tıkladığında herhangi bir yere yönlendirilmez. Bunun yerine konsol, "Belki de müziklerinden bazılarını burada çalmalıyız?" mesajını kaydeder.
Hangi diğer öğe/etkinlik kombinasyonları varsayılan işlemin engellenmesine olanak tanır? Bunların hepsini listelemem mümkün değildir ve bazen görmek için denemeniz gerekir. Ancak kısaca birkaçını aşağıda bulabilirsiniz:
<form>
öğesi + "submit" etkinliği: Bu kombinasyon içinpreventDefault()
, formun gönderilmesini engeller. Bu, doğrulama yapmak istediğinizde ve bir şey başarısız olursa formu göndermeyi durdurmak için preventDefault'u koşullu olarak çağırmak istediğinizde kullanışlıdır.<a>
öğesi + "tıklama" etkinliği: Bu kombinasyon içinpreventDefault()
, tarayıcının<a>
öğesinin href özelliğinde belirtilen URL'ye gitmesini engeller.document
+ "mousewheel" etkinliği: Bu kombinasyon içinpreventDefault()
, fare tekerleğiyle sayfa kaydırmayı engeller (klavyeyle kaydırma yine de çalışır).
↜ Bu işlem için{ passive: false }
ileaddEventListener()
çağrısı yapılması gerekir.document
+ "keydown" etkinliği: Bu kombinasyon içinpreventDefault()
ölümcüldür. Bu durum, klavyeyle kaydırma, sekme tuşuyla gezinme ve klavyeyle vurgulama işlemlerini engelleyerek sayfayı büyük ölçüde işe yaramaz hale getiriyor.document
+ "mousedown" etkinliği: Bu kombinasyon içinpreventDefault()
, fareyle metin vurgulama ve fareyle tıklama işlemiyle çağrılacak diğer "varsayılan" işlemleri engeller.<input>
öğesi + "keypress" etkinliği: Bu kombinasyon içinpreventDefault()
, kullanıcının yazdığı karakterlerin giriş öğesine ulaşmasını engeller (ancak bunu yapmayın; geçerli bir nedeni nadiren, hatta hiç olmaz).document
+ "contextmenu" etkinliği: Bu kombinasyon içinpreventDefault()
, kullanıcı sağ tıkladığında veya uzun bastığında (ya da içerik menüsünün görünebileceği başka bir şekilde) yerel tarayıcı içerik menüsünün görünmesini engeller.
Bu liste tam kapsamlı olmasa da preventDefault()
'nın nasıl kullanılabileceği hakkında iyi bir fikir vereceğini umuyoruz.
Eğlenceli bir eşek şakası mı?
Belgeden başlayarak yakalama aşamasında stopPropagation()
ve preventDefault()
simgelerine dokunursanız ne olur? Kahkaha tufanı başlar. Aşağıdaki kod snippet'i, herhangi bir web sayfasını neredeyse tamamen işe yaramaz hale getirir:
function preventEverything(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });
Bunu neden yapmak isteyeceğinizi pek bilmiyorum (belki birine şaka yapmak için olabilir) ancak burada ne olduğunu düşünmek ve neden böyle bir durum oluştuğunu anlamak faydalı olacaktır.
Tüm etkinlikler window
konumunda başlar. Bu nedenle, bu snippet'te click
, keydown
, mousedown
, contextmenu
ve mousewheel
etkinliklerinin, kendilerini dinliyor olabilecek öğelere ulaşmasını engelliyoruz. Ayrıca, bu dokümandan sonra dokümana bağlanan tüm işleyicilerin de engellenmesi için stopImmediatePropagation
işlevini çağırırız.
stopPropagation()
ve stopImmediatePropagation()
'nin (en azından çoğunlukla) sayfayı işe yaramaz hale getiren öğeler olmadığını unutmayın. Bu tür engeller, etkinliklerin normalde gidecekleri yere ulaşmasını engeller.
Ancak preventDefault()
işlevini de çağırırız. Bu işlevin, varsayılan işlemi engellediğini hatırlarsınız. Bu nedenle, tüm varsayılan işlemler (ör. fare tekerleğiyle kaydırma, klavyeyle kaydırma veya vurgulama ya da sekme oluşturma, bağlantı tıklama, bağlam menüsü görüntüleme vb.) engellenir ve sayfa oldukça işe yaramaz bir durumda kalır.
Teşekkür
Tom Wilson'ın Unsplash'teki hero resmi.