preventDefault
i stopPropagation
: kiedy używać poszczególnych metod i jak dokładnie działają.
Event.stopPropagation() i Event.preventDefault()
Obsługa zdarzeń JavaScript jest często prosta. Dotyczy to zwłaszcza prostej (względnie płaskiej) struktury HTML. Sytuacja komplikuje się, gdy zdarzenia przemieszczają się (lub rozprzestrzeniają) w hierarchii elementów. Deweloperzy zwykle kontaktują się z zespołem stopPropagation()
lub preventDefault()
, aby rozwiązać problemy, z którymi się borykają. Jeśli kiedykolwiek pomyślisz „Spróbuję preventDefault()
, a jeśli to nie zadziała, spróbuję stopPropagation()
, a jeśli to też się nie uda, spróbuję obu tych rozwiązań”, ten artykuł jest dla Ciebie. Wyjaśnię, do czego służy każda z metod, kiedy należy z niej korzystać, i przedstawię kilka przykładów jej zastosowania. Chcę raz na zawsze rozwiać Twoje wątpliwości.
Zanim jednak przejdziemy do szczegółów, warto krótko omówić 2 rodzaje obsługi zdarzeń w języku JavaScript (we wszystkich nowoczesnych przeglądarkach – Internet Explorer w wersji 9 i starszych wcale nie obsługiwał rejestrowania zdarzeń).
Style zdarzeń (przechwytywanie i przekazywanie dalej)
Wszystkie nowoczesne przeglądarki obsługują rejestrowanie zdarzeń, ale jest ono bardzo rzadko używane przez programistów.
Co ciekawe, była to jedyna forma zdarzeń obsługiwana przez Netscape. Największy konkurent Netscape, Microsoft Internet Explorer, nie obsługiwał w ogóle rejestrowania zdarzeń, ale tylko inny styl zdarzeń zwany propagowaniem zdarzeń. Gdy powstała W3C, jej członkowie uznali, że oba style zdarzeń mają swoje zalety, i ogłosili, że przeglądarki powinny obsługiwać oba, za pomocą trzeciego parametru metody addEventListener
. Pierwotnie był to tylko parametr logiczny, ale wszystkie współczesne przeglądarki obsługują obiekt options
jako trzeci parametr, za pomocą którego możesz określić (między innymi) czy chcesz korzystać z rejestrowania zdarzeń:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Obiekt options
i jego właściwość capture
są opcjonalne. Jeśli jedno z nich zostanie pominięte, wartość domyślna dla parametru capture
to false
, co oznacza, że będzie używane przenoszenie zdarzeń.
rejestrowanie zdarzeń,
Co oznacza, że odbiornik zdarzenia „nasłuchuje w fazie przechwytywania”? Aby to zrozumieć, musimy wiedzieć, jak powstają zdarzenia i jak się przemieszczają. Poniższe informacje dotyczą wszystkich zdarzeń, nawet jeśli jako deweloper nie korzystasz z nich, nie interesują Cię one lub nie myślisz o nich.
Wszystkie zdarzenia rozpoczynają się w oknie i najpierw przechodzą przez fazę przechwytywania. Oznacza to, że gdy zdarzenie zostanie wysłane, rozpoczyna okno i przechodzi „w dół” do elementu docelowego najpierw. Dzieje się tak nawet wtedy, gdy słuchasz tylko w fazie przenoszenia. Rozważ ten przykładowy kod znaczników i JavaScriptu:
<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,
);
Gdy użytkownik kliknie element #C
, wysyłane jest zdarzenie pochodzące z elementu window
. To zdarzenie będzie się rozprzestrzeniać na swoich potomkach w ten sposób:
window
=> document
=> <html>
=> <body>
=> itd., aż do osiągnięcia wartości docelowej.
Nie ma znaczenia, czy nic nie nasłuchuje zdarzenia kliknięcia w elemencie window
, document
, <html>
ani <body>
(ani w żadnym innym elemencie na drodze do celu). Zdarzenie nadal rozpoczyna się w miejscu window
i rozpoczyna swoją podróż w sposób opisany powyżej.
W naszym przykładzie zdarzenie kliknięcia będzie rozprzestrzeniać się (to ważne słowo, ponieważ będzie bezpośrednio związane z działaniem metody stopPropagation()
, co zostanie wyjaśnione w dalszej części tego dokumentu) od elementu window
do elementu docelowego (w tym przypadku #C
) przez wszystkie elementy znajdujące się między elementami window
i #C
.
Oznacza to, że zdarzenie kliknięcia rozpocznie się w miejscu window
, a przeglądarka zada te pytania:
„Czy w fazie rejestrowania coś nasłuchuje zdarzenia kliknięcia na window
?” W takim przypadku zostaną uruchomione odpowiednie moduły obsługi zdarzeń. W naszym przykładzie nic nie jest zaznaczone, więc żadne metody obsługi nie zostaną wywołane.
Następnie zdarzenie rozprzestrzeni się na document
, a przeglądarka zapyta: „Czy w fazie przechwytywania jest coś, co nasłuchuje zdarzenia kliknięcia na elemencie document
?”. Jeśli tak, zostaną wywołane odpowiednie procedury obsługi zdarzeń.
Następnie zdarzenie rozprzestrzenia się na element <html>
, a przeglądarka zapyta: „Czy w fazie rejestrowania kliknięcia elementu <html>
coś nasłuchuje?”. Jeśli tak, zostaną uruchomione odpowiednie przetwarzacze zdarzeń.
Następnie zdarzenie rozprzestrzenia się na element <body>
, a przeglądarka zapyta: „Czy w fazie przechwytywania coś czeka na zdarzenie kliknięcia elementu <body>
?”. Jeśli tak, zostaną uruchomione odpowiednie moduły obsługi zdarzeń.
Następnie zdarzenie rozprzestrzenia się na element #A
. Ponownie przeglądarka zapyta: „Czy w fazie rejestrowania coś nasłuchuje zdarzenia kliknięcia na #A
? Jeśli tak, zostaną wywołane odpowiednie moduły obsługi zdarzeń.
Następnie zdarzenie rozprzestrzenia się na element #B
(i zostanie zadane to samo pytanie).
W końcu zdarzenie dociera do celu, a przeglądarka pyta: „Czy w fazie rejestrowania ktoś nasłuchuje zdarzenia kliknięcia elementu #C
?”. Tym razem odpowiedź brzmi „tak”. Ten krótki okres czasu, w którym zdarzenie jest nastawione na cel, nazywa się „fazą docelową”. W tym momencie zostanie wywołany event handler, przeglądarka wygeneruje komunikat „#C was clicked” w console.log i to wszystko.
Nie. To jeszcze nie koniec. Proces jest kontynuowany, ale teraz przechodzi w fazę przenoszenia.
Przekazywanie zdarzeń
Przeglądarka wyświetli następujące pytania:
„Czy w fazie przenoszenia jest coś, co nasłuchuje zdarzenia kliknięcia na stronie #C
?” Zwróć na to uwagę.
Możliwe jest wychwytywanie kliknięć (lub dowolnego typu zdarzenia) zarówno w fazie przechwytywania, jak w fazie przenoszenia. Jeśli masz w obu fazach podłączone metody obsługi zdarzeń (np. wywołując .addEventListener()
dwukrotnie, raz z capture = true
i raz z capture = false
), to obie metody obsługi zdarzeń będą wywoływane w przypadku tego samego elementu. Pamiętaj też, że te reguły są wywoływane w różnych fazach (jedna w fazie przechwytywania, a druga w fazie przenoszenia).
Następnie zdarzenie rozprzestrzeni się (czyli „przejdzie” przez drzewo DOM) do elementu nadrzędnego, #B
, a przeglądarka zapyta: „Czy w fazie przenoszenia się zdarzenia są jakieś elementy, które nasłuchują zdarzeń kliknięcia w elementach #B
?”. W naszym przykładzie nic nie jest, więc żadne procedury obsługi nie zostaną wywołane.
Następnie zdarzenie będzie się przenosić do #A
, a przeglądarka zapyta: „Czy w fazie przenoszenia jest coś, co nasłuchuje zdarzeń kliknięcia na #A
?”.
Następnie zdarzenie będzie się przenosić do <body>
: „Czy w etapie propagowania jest coś, co nasłuchuje zdarzeń kliknięcia w elemencie <body>
?”.
Następnie element <html>
: „Czy w fazie przenoszenia jest coś, co nasłuchuje zdarzeń kliknięcia w elemencie <html>
?
Następnie document
: „Czy w etapie propagowania jest coś, co nasłuchuje zdarzeń kliknięcia w document
?”
Na koniec window
: „Czy w oknie w fazie przenoszenia jest coś, co nasłuchuje zdarzeń kliknięcia?”.
Uff... To była długa podróż, a nasze wydarzenie jest pewnie już bardzo zmęczone, ale uwierz lub nie, to jest droga, przez którą musi przejść każde wydarzenie. W większości przypadków nie jest to zauważalne, ponieważ deweloperów zwykle interesuje tylko jedna faza zdarzenia (zwykle jest to faza przenoszenia).
Warto poświęcić trochę czasu na zabawę z rejestracją i przekazywaniem zdarzeń oraz dodawaniem notatek do konsoli podczas uruchamiania obsługi. Warto zobaczyć ścieżkę, jaką pokonuje zdarzenie. Oto przykład, który sprawdza każdy element na obu etapach.
<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,
);
Dane konsoli zależą od tego, który element klikniesz. Jeśli klikniesz element „najgłębiej” w drzewie DOM (element #C
), zobaczysz, że wszystkie te przetwarzacze zdarzeń zostaną uruchomione. Oto element konsoli #C
(oraz zrzut ekranu) z nieco większym stylizowaniem CSS, aby było łatwiej odróżnić poszczególne elementy:
"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"
Możesz wypróbować tę funkcję w poniżej demonstracji na żywo. Kliknij element #C
i obserwuj dane wyjściowe konsoli.
event.stopPropagation()
Wiedząc już, skąd pochodzą zdarzenia i jak się przemieszczają (czyli rozprzestrzeniają) w DOM (zarówno w fazie rejestrowania, jak i w fazie propagacji), możemy teraz przyjrzeć się event.stopPropagation()
.
Metodę stopPropagation()
można wywołać w przypadku (większości) natywnych zdarzeń DOM. Piszę „w większości”, ponieważ w niektórych przypadkach wywołanie tej metody nie powoduje żadnych działań (ponieważ samo zdarzenie się nie rozprzestrzenia). Do tej kategorii należą wydarzenia takie jak focus
, blur
, load
, scroll
i kilka innych. Możesz wywołać funkcję stopPropagation()
, ale nic ciekawego się nie stanie, ponieważ te zdarzenia nie są propagowane.
Co jednak robi stopPropagation
?
Robi dokładnie to, co jest napisane. Gdy go wywołasz, przestaje się ono rozprzestrzeniać na elementy, do których w przeciwnym razie by dotarło. Dotyczy to obu kierunków (przechwytywania i przekazywania). Jeśli więc wywołasz funkcję stopPropagation()
w dowolnym miejscu w fazie przechwytywania, zdarzenie nigdy nie dotrze do fazy docelowej ani fazy dyfuzji. Jeśli wywołasz funkcję w fazie przenoszenia, będzie ona już w fazie przechwytywania, ale przestanie „przechodzić” od momentu wywołania.
Wracając do naszego przykładowego znacznika, co się stanie, jeśli wywołamy funkcję stopPropagation()
w fazie przechwytywania w elemencie #B
?
Powinien pojawić się taki wynik:
"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"
Możesz wypróbować tę funkcję w poniżej demonstracji na żywo. Kliknij element #C
w demonstracji na żywo i obserwuj dane w konsoli.
Czy można zatrzymać propagowanie na etapie #A
w ramach propagacji? Spowoduje to taki wynik:
"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"
Możesz wypróbować tę funkcję w poniżej demonstracji na żywo. Kliknij element #C
w demonstracji na żywo i obserwuj dane w konsoli.
Jeszcze jeden, dla zabawy. Co się stanie, jeśli wywołamy funkcję stopPropagation()
w fazie docelowej w przypadku #C
?
Przypomnij sobie, że „faza docelowa” to okres, w którym zdarzenie jest w docelonym stanie. Powinien pojawić się taki wynik:
"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"
Pamiętaj, że moduł obsługi zdarzenia #C
, w którym rejestrujemy „kliknięcie na #C w fazie przechwytywania”, nadal
się wykonuje, ale nie moduł, w którym rejestrujemy „kliknięcie na #C w fazie przenoszenia”. To powinno być jasne. Nazwaliśmy go stopPropagation()
, ponieważ jest to punkt, w którym propagacja zdarzenia się zakończy.
Możesz wypróbować tę funkcję w poniżej demonstracji na żywo. Kliknij element #C
w demonstracji na żywo i obserwuj dane w konsoli.
Zachęcam do wypróbowania dowolnej z tych prezentacji na żywo. Spróbuj kliknąć tylko element #A
lub tylko element body
. Spróbuj przewidzieć, co się wydarzy, a potem sprawdź, czy masz rację. W tym momencie powinieneś/powinnaś być w stanie dość dokładnie przewidywać wyniki.
event.stopImmediatePropagation()
Co to za dziwna i rzadko używana metoda? Jest to podobne do stopPropagation
, ale zamiast blokowania przekazywania zdarzenia do potomków (przechwytywanie) lub przodków (przekazywanie dalej), ta metoda ma zastosowanie tylko wtedy, gdy masz więcej niż 1 obsługę zdarzenia podłączoną do pojedynczego elementu. Ponieważ addEventListener()
obsługuje zdarzenia w stylu multicast, można wielokrotnie podłączać do jednego elementu więcej niż 1 obsługę zdarzeń. W takim przypadku (w większości przeglądarek) metody obsługi zdarzeń są wykonywane w kolejności, w jakiej zostały połączone. Wywołanie stopImmediatePropagation()
uniemożliwia wywołanie kolejnych przetwarzaczy. Na przykład:
<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,
);
W przykładzie powyżej w konsoli pojawią się takie dane wyjściowe:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
Pamiętaj, że trzeci moduł obsługi zdarzeń nigdy nie jest uruchamiany, ponieważ drugi moduł obsługi zdarzeń wywołuje funkcję e.stopImmediatePropagation()
. Gdybyśmy zamiast tego wywołali funkcję e.stopPropagation()
, trzecia obróbka nadal by się wykonała.
event.preventDefault()
Jeśli stopPropagation()
uniemożliwia zdarzeniu przemieszczanie się „w dół” (przechwytywanie) lub „w górę” (przenoszenie), co robi preventDefault()
? Wygląda na to, że tak. Czy to się zgadza?
Nie bardzo Chociaż te dwa terminy są często mylone, w istocie nie mają ze sobą wiele wspólnego.
Gdy zobaczysz preventDefault()
, w głowie dodaj słowo „działanie”. Pamiętaj, że „zapobieganie” oznacza „zapobieganie domyślnemu działaniu”.
Jakie jest domyślne działanie, o które możesz zapytać? Odpowiedź na to pytanie nie jest jednoznaczna, ponieważ zależy w dużej mierze od kombinacji elementu i zdarzenia. Co więcej, czasami nie ma w ogóle żadnego działania domyślnego.
Zacznijmy od bardzo prostego przykładu. Co się dzieje, gdy klikniesz link na stronie internetowej? Oczywiście oczekujesz, że przeglądarka przejdzie na adres URL podany w tym linku.
W tym przypadku element to tag kotwicy, a zdarzenie to zdarzenie kliknięcia. Ta kombinacja (<a>
+
click
) ma „domyślne działanie” polegające na przejściu do adresu URL linku. Co zrobić, jeśli chcesz zapobiec wykonywaniu przez przeglądarkę tego domyślnego działania? Załóżmy, że chcesz uniemożliwić przeglądarce przechodzenie do adresu URL podanego w atribute href
elementu <a>
. Oto, co preventDefault()
zrobi dla Ciebie. Przeanalizuj ten przykład:
<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,
);
Możesz wypróbować tę funkcję w poniżej demonstracji na żywo. Kliknij link The Avett Brothers i obserwuj dane w konsoli (zwróć uwagę, że nie nastąpi przekierowanie do witryny Avett Brothers).
Kliknięcie linku o nazwie The Avett Brothers spowoduje przejście do strony www.theavettbrothers.com
. W tym przypadku do elementu <a>
podłączyliśmy jednak element obsługi zdarzenia kliknięcia i określiliśmy, że działanie domyślne powinno zostać zablokowane. Gdy użytkownik kliknie ten link, nie zostanie przekierowany do żadnej strony. Konsola po prostu zarejestruje, że „Może powinniśmy odtworzyć tutaj ich muzykę”.
Jakie inne kombinacje elementów/zdarzeń pozwalają zapobiec działaniu domyślnemu? Nie sposób wymienić wszystkich, a czasami trzeba po prostu eksperymentować. Oto kilka z nich:
Element
<form>
+ zdarzenie „submit”:preventDefault()
w przypadku tej kombinacji formularz nie zostanie przesłany. Jest to przydatne, gdy chcesz przeprowadzić walidację. Jeśli coś się nie uda, możesz warunkowo wywołać metodę preventDefault, aby zatrzymać przesyłanie formularza.Element
<a>
+ zdarzenie „kliknięcie”:preventDefault()
w przypadku tej kombinacji przeglądarka nie przekierowuje do adresu URL podanego w atribute href elementu<a>
.document
+ zdarzenie „mousewheel”:preventDefault()
w tym przypadku kombinacja ta uniemożliwia przewijanie strony za pomocą kółka myszy (przewijanie za pomocą klawiatury nadal będzie działać).
↜ Do tego potrzebne jest wywołanie funkcjiaddEventListener()
z parametrem{ passive: false }
.document
+ zdarzenie „keydown”:preventDefault()
ta kombinacja jest zabójcza. Strona staje się wtedy praktycznie bezużyteczna, ponieważ nie można jej przewijać za pomocą klawiatury, używać tabulatorów ani wyróżniać klawiszy.document
+ zdarzenie „mousedown”:preventDefault()
w przypadku tej kombinacji zablokujesz wyróżnianie tekstu za pomocą myszy i wszystkie inne „domyślne” działania, które można wywołać przy wciśniętym przycisku myszy.Element
<input>
+ zdarzenie „keypress”: ta kombinacjapreventDefault()
uniemożliwi docieranie znaków wpisywanych przez użytkownika do elementu wejściowego (ale nie rób tego; rzadko, jeśli w ogóle, istnieje ku temu ważny powód).document
+ zdarzenie „contextmenu”:preventDefault()
ta kombinacja uniemożliwia wyświetlanie menu kontekstowego przeglądarki, gdy użytkownik kliknie prawym przyciskiem myszy lub naciśnie i przytrzyma przycisk (lub w dowolny inny sposób wywoła menu kontekstowe).
Ta lista nie jest w żaden sposób wyczerpująca, ale mamy nadzieję, że daje dobry pogląd na to, jak można używać funkcji preventDefault()
.
To żart?
Co się stanie, jeśli stopPropagation()
i preventDefault()
w fazie przechwytywania, zaczynając od dokumentu? Śmiech gwarantowany! Ten fragment kodu sprawi, że każda strona internetowa będzie prawie całkowicie bezużyteczna:
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 });
Nie wiem, dlaczego chcesz to zrobić (chyba że chcesz kogoś w jakiś sposób oszukać), ale warto zastanowić się, co się tu dzieje i dlaczego to powoduje taką sytuację.
Wszystkie zdarzenia pochodzą z window
, więc w tym fragmencie kodu całkowicie blokujemy dostęp zdarzeniom click
, keydown
, mousedown
, contextmenu
i mousewheel
do wszystkich elementów, które mogłyby je przechwycić. Wykonujemy też wywołanie stopImmediatePropagation
, aby wszystkie moduły obsługiwane przez ten dokument po tym wywołaniu również zostały zablokowane.
Pamiętaj, że stopPropagation()
i stopImmediatePropagation()
nie są (przynajmniej w większości przypadków) powodem, dla którego strona jest bezużyteczna. Po prostu zapobiegają one dotarciu zdarzeń do miejsca, do którego normalnie by trafiły.
Ale wywołujemy też preventDefault()
, który jak pamiętasz zapobiega domyślnemu działaniu. W efekcie wszystkie domyślne działania (np. przewijanie za pomocą kółka myszy, przewijanie za pomocą klawiatury lub wyróżnianie, klikanie linków, wyświetlanie menu kontekstowego itp.) są zablokowane, przez co strona jest praktycznie bezużyteczna.
Prezentacje na żywo
Aby zobaczyć wszystkie przykłady z tego artykułu w jednym miejscu, obejrzyj zamieszczony poniżej pokaz.
Podziękowania
Obraz główny autorstwa Tom Wilson na Unsplash.