preventDefault
i stopPropagation
: kiedy używać której metody i co dokładnie robi każda z nich.
Event.stopPropagation() i Event.preventDefault()
Obsługa zdarzeń w JavaScript jest często prosta. Jest to szczególnie ważne w przypadku prostej (względnie płaskiej) struktury HTML. Sytuacja staje się bardziej skomplikowana, gdy zdarzenia przemieszczają się (lub propagują) w hierarchii elementów. Zwykle w takich sytuacjach deweloperzy sięgają po stopPropagation()
lub preventDefault()
, aby rozwiązać napotkane problemy. Jeśli kiedykolwiek pomyślisz sobie: „Spróbuję preventDefault()
, a jeśli to nie zadziała, spróbuję stopPropagation()
, a jeśli to też nie zadziała, spróbuję obu tych metod”, ten artykuł jest dla Ciebie. Wyjaśnię dokładnie, jak działa każda metoda, kiedy jej używać i przedstawię Ci różne przykłady, które możesz sprawdzić. Moim celem jest ostateczne rozwianie Twoich wątpliwości.
Zanim jednak przejdziemy do szczegółów, warto krótko omówić 2 rodzaje obsługi zdarzeń dostępne w JavaScript (we wszystkich nowoczesnych przeglądarkach – Internet Explorer w wersjach starszych niż 9 w ogóle nie obsługiwał przechwytywania zdarzeń).
Style zdarzeń (przechwytywanie i przekazywanie)
Wszystkie nowoczesne przeglądarki obsługują przechwytywanie zdarzeń, ale deweloperzy bardzo rzadko z niego korzystają.
Co ciekawe, była to jedyna forma obsługi zdarzeń, którą pierwotnie obsługiwała przeglądarka Netscape. Największy konkurent Netscape, Microsoft Internet Explorer, w ogóle nie obsługiwał przechwytywania zdarzeń, a jedynie inny styl zdarzeń zwany propagacją zdarzeń. Gdy powstało W3C, uznano, że oba style obsługi zdarzeń są wartościowe, i zdecydowano, że przeglądarki powinny obsługiwać oba style za pomocą trzeciego parametru metody addEventListener
. Początkowo ten parametr był tylko wartością logiczną, ale wszystkie nowoczesne przeglądarki obsługują obiekt options
jako trzeci parametr, którego można użyć do określenia (między innymi), czy chcesz używać przechwytywania zdarzeń:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Obiekt options
jest opcjonalny, podobnie jak jego właściwość capture
. Jeśli któryś z nich zostanie pominięty, domyślna wartość parametru capture
to false
, co oznacza, że będzie używane propagowanie zdarzeń.
Rejestrowanie zdarzeń
Co to znaczy, że detektor zdarzeń „nasłuchuje w fazie przechwytywania”? Aby to zrozumieć, musimy wiedzieć, jak powstają zdarzenia i jak się rozprzestrzeniają. Poniższe informacje dotyczą wszystkich zdarzeń, nawet jeśli jako deweloper nie wykorzystujesz tych informacji, nie interesują Cię one ani o nich nie myślisz.
Wszystkie zdarzenia rozpoczynają się w oknie i najpierw przechodzą fazę przechwytywania. Oznacza to, że gdy zdarzenie zostanie wysłane, rozpoczyna się okno i zdarzenie przemieszcza się „w dół” w kierunku elementu docelowego w pierwszej kolejności. Dzieje się tak nawet wtedy, gdy tylko słuchasz w fazie „bąbelkowania”. Przyjrzyj się temu przykładowemu znacznikowi i kodowi JavaScript:
<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 następnie propagowane do elementów podrzędnych w ten sposób:
window
=> document
=> <html>
=> <body>
=> itd., aż dotrze do celu.
Nie ma znaczenia, czy w przypadku elementu window
, document
, <html>
lub <body>
(lub dowolnego innego elementu na drodze do celu) nie ma detektora zdarzeń kliknięcia. Zdarzenie nadal pochodzi z window
i rozpoczyna swoją podróż w opisany powyżej sposób.
W naszym przykładzie zdarzenie kliknięcia będzie się rozprzestrzeniać (to ważne słowo, ponieważ ma bezpośredni związek z działaniem metody stopPropagation()
, co wyjaśnimy w dalszej części tego dokumentu) z elementu window
do elementu docelowego (w tym przypadku #C
) przez wszystkie elementy między window
a #C
.
Oznacza to, że zdarzenie kliknięcia rozpocznie się o window
, a przeglądarka zada te pytania:
„Czy w fazie przechwytywania jest coś, co nasłuchuje zdarzenia kliknięcia w elemencie window
?”. Jeśli tak, zostaną uruchomione odpowiednie moduły obsługi zdarzeń. W naszym przykładzie nic nie jest, więc żadne moduły obsługi nie zostaną uruchomione.
Następnie zdarzenie rozprzestrzeni się na element document
, a przeglądarka zapyta: „Czy w fazie przechwytywania jest coś, co nasłuchuje zdarzenia kliknięcia na elemencie document
?”. W takim przypadku zostaną uruchomione odpowiednie procedury obsługi zdarzeń.
Następnie zdarzenie rozprzestrzeni się na element <html>
, a przeglądarka zapyta: „Czy w fazie przechwytywania jest coś, co nasłuchuje kliknięcia elementu <html>
?”. Jeśli tak, zostaną uruchomione odpowiednie procedury obsługi zdarzeń.
Następnie zdarzenie rozprzestrzeni się na element <body>
, a przeglądarka zapyta: „Czy w fazie przechwytywania na elemencie <body>
jest coś, co nasłuchuje zdarzenia kliknięcia?”. W takim przypadku zostaną uruchomione odpowiednie moduły obsługi zdarzeń.
Następnie zdarzenie rozprzestrzeni się na element #A
. Przeglądarka ponownie zapyta: „Czy w fazie przechwytywania coś nasłuchuje zdarzenia kliknięcia w #A
? Jeśli tak, zostaną uruchomione odpowiednie procedury obsługi zdarzeń.
Następnie zdarzenie rozprzestrzeni się na element #B
(i zostanie zadane to samo pytanie).
Na koniec zdarzenie dotrze do elementu docelowego, a przeglądarka zapyta: „Czy w fazie przechwytywania coś nasłuchuje zdarzenia kliknięcia w elemencie #C
?”. Tym razem odpowiedź brzmi „tak”. Ten krótki okres, w którym zdarzenie znajduje się w miejscu docelowym, jest nazywany „fazą docelową”. W tym momencie zostanie uruchomiony program obsługi zdarzeń, przeglądarka wyświetli w konsoli komunikat „#C was clicked” i to wszystko, prawda?
Źle! To jeszcze nie koniec. Proces jest kontynuowany, ale teraz przechodzi w fazę propagacji.
Przekazywanie zdarzeń w górę
Przeglądarka wyświetli pytanie:
„Czy w fazie propagacji w górę jest coś, co nasłuchuje zdarzenia kliknięcia w przypadku elementu #C
?” Zwróć szczególną uwagę na ten fragment.
Możesz nasłuchiwać kliknięć (lub dowolnego typu zdarzenia) zarówno w fazie przechwytywania, jak i propagacji. Jeśli w obu fazach masz podłączone procedury obsługi zdarzeń (np. wywołując funkcję .addEventListener()
dwukrotnie, raz z argumentem capture = true
i raz z argumentem capture = false
), to tak, obie procedury obsługi zdarzeń zostaną uruchomione dla tego samego elementu. Warto jednak pamiętać, że są one wywoływane w różnych fazach (jeden w fazie przechwytywania, a drugi w fazie propagacji).
Następnie zdarzenie będzie propagowane (częściej mówi się, że „przebiega” przez drzewo DOM, ponieważ wydaje się, że zdarzenie przemieszcza się „w górę” drzewa DOM) do elementu nadrzędnego, #B
, a przeglądarka zapyta: „Czy coś nasłuchuje zdarzeń kliknięcia w #B
w fazie przebiegu?”. W naszym przykładzie nic nie jest, więc nie zostaną uruchomione żadne procedury obsługi.
Następnie zdarzenie zostanie przekazane do elementu #A
, a przeglądarka zapyta: „Czy w fazie propagacji zdarzeń na elemencie #A
jest coś, co nasłuchuje zdarzeń kliknięcia?”.
Następnie zdarzenie zostanie przekazane do elementu <body>
: „Czy w fazie propagacji zdarzeń jest coś, co nasłuchuje zdarzeń kliknięcia w elemencie <body>
?”.
Następnie element <html>
: „Czy w fazie propagacji w górę jest coś, co nasłuchuje zdarzeń kliknięcia na elemencie <html>
?
Następnie document
: „Czy w fazie propagacji w górę jest coś, co nasłuchuje zdarzeń kliknięcia na elemencie document
?”.
Wreszcie window
: „Czy w fazie propagacji w górę jakieś elementy nasłuchują zdarzeń kliknięcia w oknie?”.
Uff... To była długa podróż, a nasze wydarzenie jest już pewnie bardzo zmęczone, ale wierz lub nie, każda zmiana stanu wydarzenia wygląda właśnie tak. Zwykle nie jest to zauważalne, ponieważ deweloperzy są zwykle zainteresowani tylko jedną fazą zdarzenia (zwykle jest to faza propagacji).
Warto poświęcić trochę czasu na eksperymentowanie z przechwytywaniem i propagacją zdarzeń oraz rejestrowaniem w konsoli notatek podczas uruchamiania funkcji obsługi. Śledzenie ścieżki zdarzenia jest bardzo przydatne. Oto przykład, w którym nasłuchiwane są wszystkie elementy w obu fazach.
<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 wyjściowe konsoli będą zależeć od klikniętego elementu. Jeśli klikniesz „najgłębszy” element w drzewie DOM (element #C
), zobaczysz, że uruchomią się wszystkie te procedury obsługi zdarzeń. Oto dane wyjściowe konsoli #C
elementu (wraz ze zrzutem ekranu) po zastosowaniu stylów CSS, aby wyraźniej 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"
event.stopPropagation()
Gdy już wiesz, skąd pochodzą zdarzenia i jak przemieszczają się (czyli rozprzestrzeniają) w DOM w fazie przechwytywania i w fazie propagacji, możesz przejść do event.stopPropagation()
.
Metodę stopPropagation()
można wywoływać w przypadku (większości) natywnych zdarzeń DOM. Używam słowa „większość”, ponieważ w przypadku niektórych z nich wywołanie tej metody nie przyniesie żadnego efektu (ponieważ zdarzenie nie jest propagowane). Do tej kategorii należą wydarzenia takie jak focus
, blur
, load
, scroll
i kilka innych. Możesz zadzwonić pod numer stopPropagation()
, ale nic ciekawego się nie wydarzy, ponieważ te zdarzenia nie są propagowane.
Ale co robi stopPropagation
?
Robi dokładnie to, co sugeruje nazwa. Po wywołaniu tego zdarzenia przestanie ono propagować do wszystkich elementów, do których w innych okolicznościach by dotarło. Dotyczy to obu kierunków (przechwytywania i przekazywania). Jeśli więc wywołasz stopPropagation()
w dowolnym miejscu w fazie przechwytywania, zdarzenie nigdy nie przejdzie do fazy docelowej ani fazy propagacji. Jeśli wywołasz go w fazie propagacji, będzie on już po fazie przechwytywania, ale przestanie się propagować od momentu, w którym go wywołasz.
Wróćmy do naszego przykładu kodu. Co się stanie, jeśli w fazie przechwytywania wywołamy stopPropagation()
w elemencie #B
?
Spowoduje to wyświetlenie tych danych wyjściowych:
"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"
A może zatrzymasz propagację na elemencie #A
w fazie propagacji w górę? Spowoduje to wyświetlenie tych danych wyjściowych:
"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"
Jeszcze jedno, dla zabawy. Co się stanie, jeśli w fazie docelowej wywołamy funkcję stopPropagation()
dla #C
?
Pamiętaj, że „faza docelowa” to nazwa okresu, w którym zdarzenie osiąga swój cel. Spowoduje to wyświetlenie tych danych wyjściowych:
"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"
Zwróć uwagę, że moduł obsługi zdarzeń dla #C
, w którym rejestrujemy „kliknięcie elementu #C w fazie przechwytywania”, nadal jest wykonywany, ale moduł, w którym rejestrujemy „kliknięcie elementu #C w fazie propagacji”, nie jest. To powinno być oczywiste. Zadzwoniliśmy z stopPropagation()
z poprzedniego, więc w tym momencie propagacja zdarzenia zostanie zakończona.
Zachęcam Cię do eksperymentowania z każdą z tych wersji demonstracyjnych na żywo. Spróbuj kliknąć tylko element #A
lub tylko element body
. Spróbuj przewidzieć, co się stanie, a potem sprawdź, czy masz rację. W tym momencie powinnaś/powinieneś być w stanie dość dokładnie przewidzieć, co się stanie.
event.stopImmediatePropagation()
Co to za dziwna i rzadko stosowana metoda? Jest podobna do metody stopPropagation
, ale zamiast uniemożliwiać zdarzeniu przejście do elementów podrzędnych (przechwytywanie) lub nadrzędnych (przekazywanie), ta metoda ma zastosowanie tylko wtedy, gdy do jednego elementu jest podłączonych więcej niż 1 procedura obsługi zdarzeń. Ponieważ addEventListener()
obsługuje zdarzenia w stylu multiemisji, można połączyć obsługę zdarzeń z jednym elementem więcej niż raz. W takim przypadku (w większości przeglądarek) procedury obsługi zdarzeń są wykonywane w kolejności, w jakiej zostały połączone. Wywołanie stopImmediatePropagation()
uniemożliwia uruchomienie kolejnych funkcji obsługi. Przyjrzyj się temu przykładowi:
<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,
);
Powyższy przykład spowoduje wyświetlenie w konsoli tych danych wyjściowych:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
Zwróć uwagę, ż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()
, trzeci moduł obsługi
i tak by się uruchomił.
event.preventDefault()
Jeśli stopPropagation()
uniemożliwia zdarzeniu przemieszczanie się „w dół” (przechwytywanie) lub „w górę” (przekazywanie), to co robi preventDefault()
? Wygląda na to, że działa podobnie. Does
it?
Nie bardzo Chociaż te dwa pojęcia są często mylone, w rzeczywistości nie mają ze sobą wiele wspólnego.
Gdy zobaczysz preventDefault()
, w myślach dodaj słowo „działanie”. Pomyśl o tym jako „zapobieganie domyślnej czynności”.
Jakie jest domyślne działanie, o które możesz poprosić? Niestety odpowiedź na to pytanie nie jest jednoznaczna, ponieważ zależy od kombinacji elementu i zdarzenia. A żeby było jeszcze bardziej skomplikowanie, czasami nie ma żadnego działania domyślnego.
Zacznijmy od bardzo prostego przykładu, aby zrozumieć, jak to działa. Czego oczekujesz, gdy klikniesz link na stronie internetowej? Oczywiście oczekujesz, że przeglądarka przejdzie do adresu URL określonego przez ten link.
W tym przypadku elementem jest tag kotwicy, a zdarzeniem jest kliknięcie. Ta kombinacja (<a>
+click
) ma „działanie domyślne” polegające na przejściu do adresu URL linku. Co zrobić, jeśli chcesz zapobiec wykonaniu przez przeglądarkę tej domyślnej czynności? Załóżmy, że chcesz uniemożliwić przeglądarce przejście do adresu URL określonego przez atrybut href
elementu <a>
. Oto co
preventDefault()
może dla Ciebie zrobić. 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,
);
Zwykle kliknięcie linku „The Avett Brothers” powoduje przejście do strony www.theavettbrothers.com
. W tym przypadku połączyliśmy jednak procedurę obsługi zdarzenia kliknięcia z elementem <a>
i określiliśmy, że należy zapobiec domyślnemu działaniu. Dlatego gdy użytkownik kliknie ten link, nie zostanie przekierowany w inne miejsce, a w konsoli pojawi się tylko komunikat „Maybe we should
just play some of their music right here instead?” (Może powinniśmy po prostu odtworzyć tutaj ich muzykę?).
Jakie inne kombinacje elementów i zdarzeń pozwalają zapobiec domyślnemu działaniu? Nie jestem w stanie wymienić wszystkich możliwości, a czasami trzeba po prostu poeksperymentować, żeby się przekonać. Oto kilka z nich:
Element
<form>
+ zdarzenie „submit”:preventDefault()
w przypadku tej kombinacji uniemożliwi przesłanie formularza. Jest to przydatne, jeśli chcesz przeprowadzić weryfikację, a w razie niepowodzenia możesz warunkowo wywołać metodę preventDefault, aby zatrzymać przesyłanie formularza.Element
<a>
+ zdarzenie „click”:preventDefault()
w przypadku tej kombinacji uniemożliwia przeglądarce przejście do adresu URL określonego w atrybucie href elementu<a>
.document
+ zdarzenie „mousewheel”:preventDefault()
ta kombinacja zapobiega przewijaniu strony za pomocą kółka myszy (przewijanie za pomocą klawiatury nadal będzie działać).
↜ Wymaga to wywołania funkcjiaddEventListener()
z parametrem{ passive: false }
.document
+ zdarzenie „keydown”:preventDefault()
w tej kombinacji jest śmiertelny. Sprawia to, że strona jest w dużej mierze bezużyteczna, ponieważ uniemożliwia przewijanie, przełączanie za pomocą klawisza Tab i wyróżnianie za pomocą klawiatury.document
+ zdarzenie „mousedown”:preventDefault()
w przypadku tej kombinacji zapobiegnie zaznaczaniu tekstu myszą i innym „domyślnym” działaniom, które można wywołać za pomocą kliknięcia myszą.Element
<input>
+ zdarzenie „keypress”:preventDefault()
w przypadku tej kombinacji uniemożliwi wpisywanym przez użytkownika znakom dotarcie do elementu wejściowego (ale nie rób tego, rzadko, jeśli w ogóle, istnieje ku temu uzasadniony powód).document
+ zdarzenie „contextmenu”:preventDefault()
ta kombinacja zapobiega wyświetlaniu natywnego menu kontekstowego przeglądarki, gdy użytkownik kliknie prawym przyciskiem myszy lub przytrzyma dłużej przycisk (lub w inny sposób, w jaki może pojawić się menu kontekstowe).
Nie jest to wyczerpująca lista, ale powinna dać Ci dobre wyobrażenie o tym, jak można używać preventDefault()
.
Zabawny kawał?
Co się stanie, jeśli w fazie przechwytywania stopPropagation()
i preventDefault()
na dokumencie? Zaczyna się zabawa! Ten fragment kodu sprawi, że każda strona internetowa stanie się niemal 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 miałbyś to robić (chyba że chcesz komuś zrobić żart), ale warto się zastanowić, co się tu dzieje i dlaczego powstaje taka sytuacja.
Wszystkie zdarzenia pochodzą z window
, więc w tym fragmencie kodu zatrzymujemy wszystkie zdarzenia click
, keydown
, mousedown
, contextmenu
i mousewheel
, aby nigdy nie docierały do żadnych elementów, które mogą ich nasłuchiwać. Wywołujemy też stopImmediatePropagation
, aby uniemożliwić działanie wszystkich procedur obsługi podłączonych do dokumentu po tej procedurze.
Pamiętaj, że stopPropagation()
i stopImmediatePropagation()
nie są (przynajmniej w większości przypadków) tym, co sprawia, że strona jest bezużyteczna. Po prostu uniemożliwiają one dotarcie zdarzeń do miejsca, do którego w innych okolicznościach by dotarły.
Wywołujemy też funkcję preventDefault()
, która, jak pamiętasz, zapobiega domyślnemu działaniu. W związku z tym wszystkie domyślne działania (takie jak przewijanie za pomocą kółka myszy, przewijanie za pomocą klawiatury, zaznaczanie, przełączanie za pomocą klawisza Tab, klikanie linków, wyświetlanie menu kontekstowego itp.) są blokowane, co sprawia, że strona jest w zasadzie bezużyteczna.
Podziękowania
Baner powitalny: Tom Wilson, Unsplash.