Zdarzenia#

Uchwyty#

Zdarzenia w postaci obiektów przemieszczają się po ścieżce propagacji, zgodnie z mechanizmem przepływu zdarzeń. To my musimy zdecydować, w którym punkcie tej ścieżki zdarzenie zostanie przechwycone i w jaki sposób obsłużone. Wykorzystujemy w tym celu uchwyt zdarzenia # (event handler), często nazywany też nasłuchem zdarzenia (event listener) czy obserwatorem zdarzenia (event observer).

Wszystkie poprzednie i aktualne specyfikacje DOM definiują tylko jeden rodzaj uchwytów zdarzeń (uchwyt przez metodę), który ma największe możliwości. W rzeczywistych projektach można zetknąć się jeszcze z dwoma starszymi mechanizmami (uchwyt przez właściwość i uchwyt przez atrybut). Ze względu na szerokie wsparcie, powszechność użycia i zgodność z HTML5 im również poświęcę nieco uwagi (szczegóły).

Już teraz nadmienię, że dla przykładów prezentowanych w kursie bardzo często będę umieszczał starsze uchwyty zdarzeń. Robię to tylko i wyłącznie ze względu na szybkość i prostotę zapisu przykładów, w rzeczywistych projektach zalecałbym jedynie sposób pierwszy, tj. uchwyt przez metodę.

Uchwyt przez metodę#

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
target.addEventListener(type, callback [, capture = false]);

Uchwyt przez metodę jest najbardziej uniwersalnym mechanizmem pozwalającym przechwycić i obsłużyć dane zdarzenie. Można go zarejestrować na każdym węźle i na niektórych obiektach, aczkolwiek obsługa konkretnego zdarzenia przez obiekt będzie ściśle określona przez daną specyfikację.

Metoda przyjmuje trzy parametry. Pierwszy oznacza nazwę zdarzenia (bez żadnych dodatkowych prefiksów). Drugi to funkcja zwrotna (nazywana też procedurą obsługi lub wywołaniem zwrotnym), czyli są to akcje, które muszą zostać wykonane po przechwyceniu zdarzenia. Do funkcji można przekazać dodatkowe argumenty, ale pierwszym zawsze jest sam obiekt zdarzenia. Trzeci określa fazę, w której zdarzenie zostanie przechwycone. Domyślne ustawiane jest bąbelkowanie.

Ze względu na nazwę metody EventTarget.addEventListener() i nazwę interfejsu EventListener definiującego wywołanie zwrotne, dla tego sposobu bardzo często będę używał po prostu określenia "nasłuch zdarzenia" i jego odmian.

Uniwersalność uchwytu przez metodę nie podlega żadnym dyskusjom. Dzięki parametrom możemy wybierać między dwoma różnymi sposobami przepływu zdarzeń, w wywołaniu zwrotnym mamy dostęp do obiektu zdarzenia, nie trzeba również pamiętać o dodawaniu do nazwy zdarzenia żadnych poprzedzających prefiksów, i w końcu możemy przechwycić każde zdarzenie udostępniane dla programisty (w przypadku pozostałych sposobów nie zawsze jest to możliwe).

Największą zaletą będzie jednak to, że wiele nasłuchów zdarzeń może obserwować to samo zdarzenie. Gdy zdarzenie pojawi się w systemie to wywołane zostaną wszystkie procedury obsługi. Obserwatorzy nie muszą wiedzieć o sobie nawzajem i mogą działać całkowicie niezależnie. Mogą podłączać się i odłączać w dowolnym momencie bez wpływania na pozostałych obserwatorów.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>

<head>

	<script>

		// Uruchom po całkowitym załadowaniu dokumentu
		window.onload = function(){

			var target = document.getElementById("button");

			// Ustawiamy dwa różne nasłuch dla tego samego zdarzenia

			target.addEventListener("click", function(e){
				alert("Pierwszy uchwyt bąbelkujący z celem zdarzenia e.target: " + e.target);
			});

			target.addEventListener("click", function(e){
				alert("Drugi uchwyt bąbelkujący z celem zdarzenia e.target: " + e.target);
			}, false);

		}

	</script>

</head>

<body>

	<p>Kliknij w przycisk by wywołać dwie różne procedury obsługi zdarzenia.</p>
	<input id="button" type="button" value="Kliknij mnie!">

</body>

</html>

Warto przypomnieć, że kolejność wywoływania procedur obsługi niekoniecznie musi być zgodna z kolejnością poleceń umieszczanych w kodzie skryptu.

Na dzień dzisiejszy jest to najwłaściwszy sposób obsługi zdarzeń, stosowany w nowoczesnym kodzie przez najlepszych programistów i obsługiwany przez wszystkie aktualne przeglądarki. Wszędzie tam, gdzie to tylko możliwe, zachęcam do jego stosowania.

Więcej szczegółowych informacji dla tego sposobu pojawi się w dalszych miejscach kursu.

Uchwyt przez właściwość#

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
target.ontype = callback;

Uchwyt przez właściwość jest kolejnym sposobem przechwytu zdarzenia, ale o znacznie mniejszych możliwościach, jeśli porównamy go z uchwytem przez metodę. Wykorzystujemy tutaj fakt, że większość obiektów posiada właściwości tożsame z nazwami zdarzeń, które poprzedza się specjalnym prefiksem on-. Dla przykładu, złapanie zdarzenia click wykonujemy za pomocą właściwości onclick. Wymóg stosowania dodatkowego prefiksu sam w sobie jest pewnym utrudnieniem, o którym nie należy zapominać.

Kolejną ułomnością będzie fakt, że sposób ten nie umożliwia łatwego przypisania wielu procedur obsługi dla tego samego zdarzenia; każde przypisanie nowej funkcji do właściwości powoduje całkowite usunięcie funkcji poprzedniej. Oczywiście można przed przypisaniem nowej funkcji sprawdzić, czy właściwość zawiera już jakąś funkcje, i jeśli tak, dodać tę istniejącą jako część nowej, a następnie przypisać do właściwości nową funkcje. Z opisu tego procesu można wywnioskować, że całość wymaga sporo zachodu po stronie programisty.

Jeszcze jednym minusem jest brak możliwości wyboru, w którym miejscu ścieżki propagacji zdarzenie zostanie przechwycone, a każda przeglądarka może to robić według własnego uznania. W praktyce większość przeglądarek wybiera w tym przypadku bąbelkowanie.

Należy pamiętać także o tym, że nie wszystkie zdarzenia mają swoje właściwościowe odpowiedniki, jak chociażby bardzo popularne zdarzenie DOMContentLoaded z HTML5. W takich przypadkach możemy skorzystać tylko i wyłącznie z uchwytu przez metodę.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>

<head>

	<script>

		// Uruchom po całkowitym załadowaniu dokumentu
		window.onload = function(){

			var target = document.getElementById("button");

			target.onclick = function(e){
				alert("Uchwyt bąbelkujący z celem zdarzenia e.target: " + e.target);
			}

		}

	</script>

</head>

<body>

	<p>Kliknij w przycisk by wywołać procedurę obsługi zdarzenia.</p>
	<input id="button" type="button" value="Kliknij mnie!">

</body>

</html>

Uchwyt przez właściwość (podobnie zresztą jak uchwyt przez atrybut) jest spuścizną po starszych specyfikacjach HTML, ale ze względu na kompatybilność wsteczną jest dalej opisywany w najnowszym HTML5. Jego stosowanie nie jest błędem, w zasadzie cały kod umieszczony zostaje tam gdzie powinien (po stronie ECMAScript), dlatego podział na warstwy zostaje zachowany. Czasami szybciej i wygodniej podczepić obsługę zdarzenia do właściwości, niż wypisywać analogiczną metodę nasłuchu zdarzenia ze wszystkimi jego parametrami. Trzeba jedynie zdawać sobie sprawę ze wszystkich ograniczeń tego sposobu.

Uchwyt przez atrybut#

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<znacznik ontype="callback">...</znacznik>

Uchwyt przez atrybut jest najstarszym i najprostszym sposobem reakcji na zdarzenie, stosowanym głównie przez początkujących programistów. Wystarczy dla znacznika otwierającego (bezpośrednio w kodzie HTML) zadeklarować odpowiedni atrybut tożsamy z nazwą zdarzenia, którego nazwę poprzedza się specjalnym prefiksem on-, po czym przypisać mu konkretną wartość. Co prawda nie zostaje jawnie utworzona funkcja obsługi zdarzenia, jednak taka funkcja jest tworzona w tle. Zawiera ona kod będący wartością tego atrybutu.

Sposób ten zawiera identyczne ograniczenia, jakie wypisałem w przypadku uchwytu przez właściwość. Sam obiekt zdarzenia znajduje się pod zmienną o nazwie event udostępnianą bezpośrednio w wartości atrybutu (żadna inna nazwa nie będzie brana pod uwagę). W niektórych przeglądarkach jest to również zmienna globalna i może być pobierana jako właściwość obiektu globalnego window.event (np. Chrome, Opera, IE, ale nie Firefox).

Na dokładkę zaburzony zostaje podział na warstwy, w związku z czym uchwyt przez atrybut nigdy nie powinien występować w kodzie produkcyjnym. Przydaje się jedynie do szybkiego sytuacyjnego testowania pewnych fragmentów HTML w chwili, kiedy dostawienie znacznika skryptu i umieszczenie w nim analogicznych poleceń zajęłoby zdecydowanie więcej czasu.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>

<head>
</head>

<body>

	<p>Kliknij w przycisk by wywołać procedurę obsługi zdarzenia.</p>
	<input type="button" value="Kliknij mnie!" onclick="alert('Uchwyt bąbelkujący z celem zdarzenia event.target: ' + event.target)">

</body>

</html>

Oczywiście w wartościach atrybutów można umieszczać wywołania naszych własnych funkcji pochodzących z globalnego zasięgu, z ewentualnym przekazaniem różnych argumentów:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>

<head>

	<script>

		function handled(context, e){

			alert("Uchwyt bąbelkujący z celem zdarzenia e.target: " + e.target
				+ "\n\n this z funkcji obsługi: " + context);

		}

	</script>

</head>

<body>

	<p>Kliknij w przycisk by wywołać procedurę obsługi zdarzenia.</p>
	<input type="button" value="Kliknij mnie!" onclick="handled(this, event)">

</body>

</html>

Rekonstrukcja uchwytów#

W obrębie DOM istnieją polecenia, za pomocą których możliwe jest powielenie lub odtworzenie stanu danego węzła. Warto wymienić chociażby:

Powyższe metody nie zapamiętują uchwytów zdarzeń przypisanych do węzłów. Jedynym wyjątkiem będzie uchwyt przez atrybut, czyli atrybut umieszczany bezpośrednio w kodzie znacznikowym strony. Atrybuty w elementach podlegają wszelkim rekonstrukcjom i tym samym dotyczy to także reprezentowanych przez nie uchwytów zdarzeń. Pozostałe rodzaje uchwytów, czyli uchwyty przez metodę i uchwyty przez właściwość, będą pomijane. W razie konieczności należałoby je samodzielnie odtworzyć, co wiąże się z ich ponownym zarejestrowaniem.

Wbrew pozorom omawiane zachowanie może być niezwykle przydatne. Wymienione polecenia jako jedyne umożliwiłyby pozbycie się zarejestrowanych nasłuchów zdarzeń, do których przekazano anonimowe procedury obsługi. Metoda EventTarget.removeEventListener() dla tego konkretnego przypadku nie spełniłaby swojej funkcji. Warto o tym pamiętać, szczególnie jeśli ktoś próbuje manipulować kodem skryptów w nie swoich stronach internetowych.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>

<head>

	<script>

		// Uruchom po całkowitym załadowaniu dokumentu
		window.onload = function(){

			document.getElementById("p2").onclick = function(){alert(this.textContent)}; // uchwyt zdarzenia dodany w kodzie JS

			document.body.appendChild(document.getElementById("ul1").cloneNode(true)); // głęboka kopia listy
			document.body.appendChild(document.getElementById("p1").cloneNode("true")); // głęboka kopia pierwszego akapitu
			document.body.appendChild(document.getElementById("p2").cloneNode("prawda")); // głęboka kopia drugiego akapitu
			document.body.appendChild(document.getElementById("p2").cloneNode(false)); // płytka kopia drugiego akapitu

		}

	</script>

</head>

<body>
	<p>Prosta lista UL z różnymi węzłami:</p>

	<ul id="ul1">
		<li><a href="">Pierwszy odsyłacz</a></li>
		<li><a href="">Drugi odsyłacz</a></li>
		<li><a href="">Trzeci odsyłacz</a></li>
	</ul>

	<p id="p1" onclick="alert(this.textContent)">Akapit 1 z atrybutem onclick w kodzie HTML (kliknij!).</p>
	<p id="p2" style="border: solid 1px red">Akapit 2 z właściwością onclick w kodzie JS (kliknij!).</p>

	<h1>Kopie (klony)</h1>
</body>

</html>

Odniesienia w HTML5#

W HTML5 stosowane jest nieco inne nazewnictwo, chociaż z praktycznego punktu widzenia całości będzie identyczna. Oto przełożenie mojej terminologii na tę stosowaną w HTML5 i D3E/DOM4:

Ze względu na prostotę i już stosowaną konwencję najczęściej będę stosował moje nazewnictwo.

Jak wspomniałem nieco wcześniej uchwyt przez metodę to obecnie najwłaściwszy sposób łapania zdarzeń. Jego użycie będzie generowało tzw. nasłuch/uchwyt zdarzenia z trzema cechami: typem, funkcją zwrotną oraz przechwytywaniem.

We wczesnym języku HTML istniały specjalne atrybuty dla zdarzeń, które umieszczało się wprost w strukturze znacznikowej (np. <p onclick=""></p>). Struktura znacznikowa ma swoje odzwierciedlenie w DOM, dlatego atrybuty ze znaczników były dostępne z poziomu kodu JS przy użyciu właściwości o identycznych nazwach. Ze względu na zachowanie wstecznej kompatybilności najnowszy HTML5 definiuje jedynie te dwa starsze podejścia, a nasłuchy zdarzeń traktowane są w kontekście odwołań do najnowszej specyfikacji DOM.

Kolejność przetwarzania#

Najważniejszą kwestią będzie to, że dla tego samego zdarzenia zarówno uchwyt przez właściwość jak i uchwyt przez atrybut reprezentują jeden i ten sam uchwyt zdarzenia, który w praktyce będzie przekształcany do nasłuchu zdarzenia z przechwytywaniem ustawianym na boolowskie false. Domyślnie wartością początkową wszystkich tych uchwytów jest null:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	document.write(window.onload); // null
	document.write("<br>");
	document.write(document.documentElement.onclick); // null
	document.write("<br>");
	document.write(document.documentElement.onfocus); // null

</script>

Z racji tego, że nasłuchy zdarzeń są wykonywane zgodnie z kolejnością rejestracji, to zasady przekształcania starszych sposobów do nasłuchów zdarzeń będą istotne z perspektywy kolejności ich wykonywania.

Reguła jest prosta; każde ustawienie starszego sposobu na wartość inną niż null (musi być obiekt) powoduje, że dla tego sposobu wygenerowany zostanie nasłuch zdarzenia w miejscu jego wystąpienia i dodany do kolejki. Późniejsze przedefiniowanie uchwytu nie zmieni jego kolejności względem innych nasłuchów zdarzeń, zmieni się jedynie wywoływana procedura obsługi. Zamiast zawiłych regułek najlepiej przeanalizować jeden krótki przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<button id="test">Kliknij w przycisk!</button>

<script>

	var btn = document.getElementById("test");

	btn.onclick = {}; // zajmujemy kolejkę dla starszego uchwytu

	btn.addEventListener("click", function() {alert("Drugi");}, false);

	btn.addEventListener('click', function () {alert("Trzeci");}, false);

	btn.onclick = function() {alert("Pierwszy");};

	btn.addEventListener('click', function(){alert("Czwarty");}, false);

</script>

To samo zachowanie będzie dotyczyło uchwytów przez atrybut w kodzie znacznikowym strony, chociaż w ich przypadku istotne będzie również położenie samego elementu z atrybutem względem elementu skryptowego z innymi uchwytami:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<button id="test" onclick>Kliknij w przycisk!</button>

<script>

	var btn = document.getElementById("test");

	btn.addEventListener("click", function() {alert("Drugi");}, false);

	btn.addEventListener('click', function () {alert("Trzeci");}, false);

	btn.onclick = function() {alert("Pierwszy");};

	btn.addEventListener('click', function(){alert("Czwarty");}, false);

</script>

Takie są wymagania HTML5 i warto o nich pamiętać (starsze sposoby obsługi zdarzeń nie tracą na popularności). Trzeba jednak podkreślić, że na chwilę obecną jedynie przeglądarki Firefox i IE11 przestrzegają prawidłowej kolejności przetwarzania wszystkich uchwytów zdarzeń (niestety Chrome robi to po swojemu).

Pasek społecznościowy

SPIS TREŚCI AKTUALNEJ STRONY

Zdarzenia (H1) Uchwyty (H2) Uchwyt przez metodę (H3) Uchwyt przez właściwość (H3) Uchwyt przez atrybut (H3) Rekonstrukcja uchwytów (H3) Odniesienia w HTML5 (H3) Kolejność przetwarzania (H4)