Szczegółowa analiza zdarzeń JavaScript

preventDefaultstopPropagation: 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ż nie zadziała, spróbuję obie te opcje”, ten artykuł jest dla Ciebie. Wyjaśnię, na czym polega każda z nich, kiedy należy z niej korzystać, i przedstawię różne przykłady 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 (w przypadku wszystkich nowoczesnych przeglądarek – 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 przechwytywania zdarzeń, ale tylko inny styl zdarzeń zwany propagowaniem zdarzeń. Gdy powstała W3C, jej członkowie uznali, że oba style tworzenia 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 pominiesz jedno z tych pól, domyślną wartością parametru capture będzie false, co oznacza, że zostanie użyte 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 ani 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). Wydarzenie nadal rozpoczyna się w miejscu window i podróżuje 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(), o której opowiemy później w tym dokumencie) od elementu window do elementu docelowego (w tym przypadku #C) przez wszystkie elementy znajdujące się między elementami window#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 metody 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 jest coś, co nasłuchuje zdarzenia kliknięcia elementu <body>?”. Jeśli tak, zostaną uruchomione odpowiednie moduły obsługi zdarzeń.

Następnie zdarzenie rozprzestrzeni 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 śledzenie kliknięć (lub dowolnego typu zdarzenia) zarówno w fazie przechwytywania, jak i 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, co jest nazywane „przekazywaniem dalej”) do elementu nadrzędnego, #B, a przeglądarka zapyta: „Czy w fazie przekazywania dalej zdarzeń kliknięcia w elemencie #B jest zarejestrowany jakiś element odbiorczy?”. W naszym przykładzie nic nie jest zaznaczone, więc nie zostaną wywołane żadne procedury obsługi.

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 fazie przenoszenia zdarzeń 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 na dodawanie 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 wyjściowe 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 prezentowanym demonstracyjnym filmie. 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 zająć 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 propagacji. 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?

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 prezentowanym demonstracyjnym filmie. Kliknij element #C w demonstracji na żywo i obserwuj dane w konsoli.

Czy można zatrzymać propagowanie na etapie #A w ramach procesu propagowania? 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()fazie docelowej w przypadku #C? Przypomnij sobie, że „faza docelowa” to okres, w którym zdarzenie jest w docelonym stanie. 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 prezentowanym demonstracyjnym filmie. 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 się nie mylisz. W tym momencie powinieneś/powinnaś być w stanie dość dokładnie przewidywać wyniki.

event.stopImmediatePropagation()

Co to za dziwna i rzadko używana metoda? Ta metoda jest podobna do stopPropagation, ale zamiast blokowania przekazywania zdarzenia do potomków (przechwytywania) lub przodków (przekazywania dalej) działa 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 działa?

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 domyślne działanie możesz zastosować? 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 zablokować przeglądarce przechodzenie do adresu URL określonego w atributach <a> elementu href. 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 prezentowanym demonstracyjnym filmie. 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 jakąś muzykę”.

Jakie inne kombinacje elementów i zdarzeń pozwalają zapobiec działaniu domyślnemu? Nie sposób wymienić wszystkich, a czasami trzeba po prostu eksperymentować. Oto kilka krótkich informacji:

  • 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 przypadku tej kombinacji kolumna nie będzie się przewijać za pomocą kółka myszy (przewijanie za pomocą klawiatury będzie jednak działać).
    ↜ W tym celu należy wywołać funkcję addEventListener() 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ć po naciśnięciu myszy.

  • Element <input> + zdarzenie „keypress”: ta kombinacja preventDefault() 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, contextmenumousewheel do wszystkich elementów, które mogłyby je przechwycić. Wykonujemy też wywołanie stopImmediatePropagation, aby wszystkie moduły obsługi zaimplementowane w dokumencie po tym wywołaniu również zostały zablokowane.

Pamiętaj, że stopPropagation()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 kółkiem myszy, przewijanie klawiaturą lub wyróżnianie, klikanie linków, wyświetlanie menu kontekstowego itp.) są zablokowane, przez co strona jest w większości bezużyteczna.

Prezentacje na żywo

Aby zobaczyć wszystkie przykłady z tego artykułu w jednym miejscu, obejrzyj załączony poniżej pokaz.

Podziękowania

Obraz główny autorstwa Tom Wilson na Unsplash.