JavaScript 再入門(その28) イベントの操作

イベントの操作

イベントとは?

JavaScript 再入門なので、今更こんな説明は必要ないと思われますが、概念的には理解できていても、いざ説明しようとすると難しいものです。

MDNの「イベントへの入門」には次のように説明されています。

イベントは、あなたがプログラムを書いているシステムで生じた動作、出来事を指します。
システムからあなたへ、イベントとして何かあった事を知らせてくるので、必要であればそれに何らかの反応を返す事ができます。

イベントは、あなたがプログラミングしているシステムで起こることで、あなたのコードがそれらに反応できるようにシステムがあなたに伝えるものです。
developer.mozilla.org

例えば、ユーザーがウェブページ上のボタンクリック、フォームへの入力、ページの読み込み、マウスポインタが要素上の通過など、様々な動作をイベントと呼び、それらのイベントをブラウザが DOM を通じてを通知するので、それに対してどう振る舞うかをプログラミングする手法です。


イベントの3つの書式

イベントを取得する方法には、インラインイベントハンドラ・イベントハンドラ・イベントリスナの3つの書式があります。
ボタンをクリックすると、要素の背景色がランダムに設定される簡単な例でそれぞれの書式を説明します。

クリックイベントを取得時に、背景色をランダムに設定する関数 bgChange() を呼び出します。

<html>
	<head>
		<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
		<style type="text/css">
			main { text-align: center; }
			p {
				padding: 3.5em;
				display: inline-block;
				border: solid 1px black;
			}
		</style>
	</head>
	<body>
		<main id="contents_id" class="contents_class">
			<h3>JavaScript 再入門</h3>
			<div>
				<p id="p">ボタンを押すと色が変わるよ。</p>
			</div>
			<div>
				<button id="btn" type="button">ボタン</button>
			</div>
		</main>
	</body>
</html>

このスクリプトはイベントリスナの書式で書かれています。

<script>
	const btn = document.querySelector('#btn') ;	// ボタン要素の取得
	const p = document.querySelector('#p') ;		// 文章要素を取得
	// 背景色をランダムに設定
	const bgChange = () => {
		p.style.backgroundColor =  'rgb(' + getRandomInt(255) + ',' + getRandomInt(255) + ',' + getRandomInt(255) + ')';
	}
	// ランダムな整数を求める
	const getRandomInt = (max) => {
		max = Math.floor(max);
		return Math.floor(Math.random() * (max));
	}
    btn.addEventListener( 'click' , bgChange ) ;
</script>

インラインイベントハンドラ

HTML属性に直接イベントハンドラを書き込む書式です。

<button id="btn" type="button" onclick="bgChange()">ボタン</button>

HTML と JavaScript が混在するので、メンテナンスが非常に困難になります。
HTMLは文書定義・CSSはスタイル定義・JavaScriptはクライアントの動作定義と、切り分けることがメンテナンス性の向上につながりますので、インラインイベントハンドラでのコーディングはお勧めしません。

イベントハンドラ

要素のイベントオブジェクトのプロパティに処理を登録します。

btn.onclick = bgChange ;

null または ” を登録することで、イベントハンドラの処理を削除することができます。

HTML と JavaScript の分離はできるのですが、イベントハンドラに登録できる処理は一つだけです。
複数回登録すると、最後に登録した処理が実行されます。

window.onload = func1 ;
window.onload = func2 ;		// func1 が上書きされて func2 のみが実行される。

スクリプトを複数のモジュールに分割して開発した場合等に問題となることがあります。
上記例のように、onload イベントハンドラに、メインのスクリプトとモジュールのスクリプトが登録をした場合、どちらか一方が実行されない事態になります。しかし、デフォルトの処理をイベントハンドラに登録しておき、ある条件の時には違う処理をしたい場合などはイベントハンドラは管理がしやすいくコードも少なくすみます。

イベントリスナ

要素のイベントリスナに処理を登録します。

btn.addEventListener( 'click' , bgChange ) ;

イベントリスナは複数の処理を登録することができます。

window.addEventListener( 'load' , func1 ) ;  //
window.addEventListener( 'load' , func2 ) ;  // func1 , func2 共実行されます。

イベントリスナから処理を削除する場合は removeEventListener を使います。

btn.removeEventListener( 'click' , bgChange ) ;

イベントの3つの書式で、最もお勧めする書式はイベントリスナです。
複数の処理を登録できる以外にも、addEventListener は第三引数によって、イベントの伝播方法の挙動指定(キャプチャフェーズ/ターゲットフェーズ/バブリングフェーズ)をはじめとする様々なオプションを設定する事ができます。


addEventListener

ここからは、イベントリスナを登録する addEventListener を説明していきます。

addEventListener(type, listener, options);
addEventListener() は EventTarget インターフェイスのメソッドで、ターゲットに特定のイベントが配信されるたびに呼び出される関数を設…
developer.mozilla.org

イベントリスナのコールバック

コールバックの this は「JavaScript 再入門(その13) 関数」でも説明しましたが、アロー関数とそれ以外では値が違いますので注意が必要です。

JavaScript 再入門(その13) 関数
関数 関数の宣言 JavaScript には、関数の宣言方法が、大きく分けて3つの方法があります。 function を利用した関数宣言無名関数(関数式)での宣…
sakura-system.com

アロー関数以外のコールバックでは、this はイベントを配信した要素を示します。

	const btn = document.querySelector('#btn') ;	// ボタン要素の取得
	const p = document.querySelector('#p') ;		// 文章要素を取得
	function bgChange() {
		this.style.color = 'green' ;	// ボタンの文字色が緑色になる this がイベントを配信したボタンになっている。
		p.style.backgroundColor = 'rgb(' + getRandomInt(255) + ',' + getRandomInt(255) + ',' + getRandomInt(255) + ')';
	}
	btn.addEventListener( 'click' , bgChange ) ;

先例のコールバック関数を通常関数に変えると、イベントを配信したボタンに対して this を使ってアクセスすることができます。

アロー関数でイベントを配信した要素に対してアクセスする場合は、イベントオブジェクトの currentTarget プロパティを参照することでアクセスできます。

	const btn = document.querySelector('#btn') ;	// ボタン要素の取得
	const p = document.querySelector('#p') ;		// 文章要素を取得
	const bgChange = (e) => {
		e.currentTarget.style.color = 'red' ;	// ボタンの文字色が赤色になる イベントオブジェクトからイベント発信元を取得
		p.style.backgroundColor =  'rgb(' + getRandomInt(255) + ',' + getRandomInt(255) + ',' + getRandomInt(255) + ')';
	}
	btn.addEventListener( 'click' , bgChange ) ;

イベントリスナのデータの出し入れ

イベントリスナーは引数をイベントオブジェクトしかとりません。返り値は無視されます。
では、どうやって値を渡したり返したりすればよいでしょうか。

色々とやり方はあると思うのですが、オブジェクトまたはクラスを使った方法を示します。
オブジェクトのプロパティを参照することで、コールバックにデータを渡してみます。

	const btn = document.querySelector('#btn') ;	// ボタン要素の取得
	const p = document.querySelector('#p') ;		// 文章要素を取得
	const handleObj = {
		counter : 0 ,
		title : 'オブジェクトのデータだよ ' ,
		// 背景色をランダムに設定
		bgChange : function (e) {
			++this.counter ;
			e.currentTarget.style.color = 'red' ;
			p.style.backgroundColor =  'rgb(' + getRandomInt(255) + ',' + getRandomInt(255) + ',' + getRandomInt(255) + ')';
			p.innerText = this.title + this.counter ;	// this は、オブジェクト内のプロパティを参照している。
		}
	}
	btn.addEventListener( 'click' , (e) => { handleObj.bgChange(e) } ) ;		// ここでアロー関数で呼び出すので this は決まらない

アロー関数は、this を持たないので、その外側のスコープのを参照しますのでthis は handleObj オブジェクトとなりますので、オブジェクト handleObj のプロパティを使ってデータの出し入れを可能にします。

オプション

オプションはオブジェクトで渡されます。

once (Boolean)

イベントリスナが一度呼び出されると、自動的に削除されます。

	btn.addEventListener( 'click' , bgChange , { once : true } ) ;

「ボタン」を1回クリックすると、イベントリスナから削除されるので、2回目以降のクリックではイベントが発生せず、色が変わらなくなります。

signal (AbortSignal インターフェイス)

AbortSignal オブジェクトの abort() メソッドが呼び出された時に、リスナーが削除されます。

	const btn = document.querySelector('#btn') ;	// ボタン要素の取得
	const p = document.querySelector('#p') ;		// 文章要素を取得
	const controller = new AbortController() ;		// イベントリスナー中断コントローラ ***
	let cnt = 0 ;
	// 背景色をランダムに設定
	const bgChange = () => {
		p.style.backgroundColor = 'rgb(' + getRandomInt(255) + ',' + getRandomInt(255) + ',' + getRandomInt(255) + ')';
		if ( ++cnt >= 5 ) {
			controller.abort() ;				// 5回イベントが発火するとイベントリスナーを削除 ***
		}
	}
	// ランダムな整数を求める
	const getRandomInt = (max) => {
		max = Math.floor(max);
		return Math.floor(Math.random() * (max));
	}
	btn.addEventListener( 'click' , bgChange , { signal : controller.signal } ) ;	// シグナルに中断コントローラを指定 ***

この例では、イベントリスナ登録時に signal プロパティに中断コントローラ (AbortController) の signal プロパティを登録しています。中断コントローラは abort() メソッドが呼び出されると signal プロパティが登録されたプロパティに中断リクエストを渡します。

「ボタンを」5回クリックされると中断コントローラの abort() メソッドが呼び出されるので、イベントリスナから削除され、5回目以降のクリックではイベントが発生せず、色が変わらなくなります。

イベントの伝播の3つのフェーズ キャプチャ・ターゲット・バブリング (capture)

イベントを詳しく説明する場合に重要なのが、「イベントの伝播」です。
イベントの伝播とは、どのようにしてブラウザがイベントを処理し、イベントが発火した要素を特定して、イベントを配信する手順です。

イベントの伝播には、3つのフェーズがあり、キャプチャフェーズ・ターゲットフェーズ・バブリングフェーズがあり、この順序で各フェーズが実行されます。

ターゲットフェーズは、イベントが発火した要素を特定して、イベントを配信するフェーズです。
キャプチャフェーズの終点であり、バブリングフェーズの始点でもあります。

キャプチャフェーズを説明するために次の例を作成しました。

<html>
	<head>
		<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
		<style type="text/css">
			main { text-align: center; }
			.item {
				padding: 2.0em;
				border: solid 1px black;
			}
			#p { background-color:red ; }
			#c { background-color:yellow ; }
			#gc { background-color:green ; }
			#mess {
				margin: 0.5em 0 ;
				padding: 0.5em;
				border: solid 1px black;
			}
		</style>
	</head>
	<body>
		<main id="contents_id" class="contents_class">
			<h3>JavaScript 再入門</h3>
			<h4>キャプチャフェーズ</h4>
			<div id="p" class="item">
				親
				<div id="c" class="item">
					子
					<div id="gc" class="item">孫</div>
				</div>
			</div>
			<div id="mess">&nbsp;</div>
		</main>
	</body>

</html>
<script>
	const h = document.querySelector('html') ;		// html要素の取得
	const b = document.querySelector('body') ;		// body要素の取得
	const m = document.querySelector('main') ;		// main要素の取得
	const p = document.querySelector('#p') ;		// 親要素の取得
	const c = document.querySelector('#c') ;		// 子要素を取得
	const gc = document.querySelector('#gc') ;		// 孫要素を取得
	const mess = document.querySelector('#mess') ;	// メッセージ表示要素を取得
	// イベントコールバック関数
	const windowdmess = () => { dmess( 'window' ) ; }
	const htmldmess = () => { dmess( 'html' ) ; }
	const bodydmess = () => { dmess( 'body' ) ; }
	const maindmess = () => { dmess( 'main' ) ; }
	const pdmess = () => { dmess( '親' ) ; }
	const cdmess = () => { dmess( '子' ) ; }
	const gcdmess = () => { dmess( '孫' ) ; }
	// メッセージの表示関数
	const dmess = ( nm ) => {
		let val = nm ;
		if ( mess.innerText.trim() != '' ) {
			val = ' - ' + nm ;
		}
		mess.innerText += val ;
		console.log( nm ) ;
	}
	// addEventListener の第三引数
	const thirdOpt =  { capture:true } ;
	// イベントリスナ登録
	window.addEventListener( 'click' , windowdmess , thirdOpt ) ;
	h.addEventListener( 'click' , htmldmess , thirdOpt ) ;
	m.addEventListener( 'click' , maindmess , thirdOpt ) ;
	b.addEventListener( 'click' , bodydmess , thirdOpt ) ;
	p.addEventListener( 'click' , pdmess , thirdOpt ) ;
	c.addEventListener( 'click' , cdmess , thirdOpt ) ;
	gc.addEventListener( 'click' , gcdmess , thirdOpt ) ;
</script>

このスクリプトでは、window と html / body / main タグと親 / 子 / 孫の div 要素にマウスクリックのイベントリスナを登録しています。addEventListener の第三引数を { capture:true } にしていますので、キャプチャフェーズでイベントが発生するようになっています。

緑の要素「孫」をクリックしてみてください。

「window – html – body – main – 親 – 子 – 孫」

と表示されます。

次にバブリングフェーズを見るために addEventListener の第三引数を何も指定しない、デフォルトの状態にしてみましょう。

	// addEventListener の第三引数
	const thirdOpt =  {} ;

緑の要素「孫」をクリックしてみてください。
キャプチャフェーズとは逆にイベントが伝播しているのがわかります。

「孫 – 子 – 親 – main – body – html – window」

と表示されます。

一つの要素のイベントが、関係する他の要素にイベントが伝播しています。
「孫」要素をクリックした時の、通常( addEventLitener の第三引数が指定されていない )の動作は以下のようになります。

  • window がクリックイベントを検出。
  • イベントの発火したイベントターゲットが含まれる要素をキャプチャしながら html → body → main → 親 → 子と子要素をたどり検出する。(キャプチャフェーズ)
  • イベントターゲットに到達したらイベントを配信(dispatch)する。(ターゲットフェーズ)
  • イベントを配信しながら親要素をたどり window までもどる。(バブリングフェーズ)

第三引数を { capture: true } にすると、通常のイベントの配信とは逆の、キャプチャフェーズでイベントを配信しながら伝播して、バブリングフェーズではイベントの配信をしません。

先程の例を改良してキャプチャとバブリングの復習をしてみましょう。
親 – 子 – 孫の入れ子になった要素それぞれの、イベントリスナにクリックイベントが登録されています。
クリアボタンをクリックすれば、メッセージをクリアすることができます。

まずは通常のバブリングでのイベント配信です。

	c.addEventListener( 'click' , cdmess ) ;

次にキャプチャリングです。

	c.addEventListener( 'click' , cdmess , { capture:true } ) ;

バブリングとキャプチャリングを使い分けることにより、イベントの処理順序をコントロールすることができます。

パッシブモード (passive)
document.addEventListener('scroll', func, { passive: true });

主に wheel / scroll / touchmove 等連続して発生するイベントについては、イベントリスナで指定された関数が preventDefault() を呼び出さないことを明示します。
パッシブモードを明示することにより、既定の動作がスムーズに動作します。
これは、イベントリスナで指定された関数内に preventDefault() の有無の判定が、指定関数を全て処理されてからでないと判定できないからです。パッシブモードであれば、指定関数の処理が終わる前に規定の動作を発行するので、規定の動作がスムーズになります。


イベントの伝播を抑止 Event.stopPropagation()

イベントオブジェクトの stopPropagation() メソッドを実行することにより、イベントのキャプチャリングやバブリングによるイベントの伝播を抑止することができます。

<script>
	const p = document.querySelector('#p') ;		// 親要素の取得
	const c = document.querySelector('#c') ;		// 子要素を取得
	const gc = document.querySelector('#gc') ;		// 孫要素を取得
	const mess = document.querySelector('#mess') ;	// メッセージ表示要素を取得
	const btn = document.querySelector('#btn') ;	// ボタン要素を取得
	// イベントコールバック関数
	const pdmess = (e) => { dmess( '親' , e ) ; }
	const cdmess = (e) => { dmess( '子' , e ) ; }
	const gcdmess = (e) => { dmess( '孫' , e ) ; }
	// メッセージの表示関数
	const dmess = ( nm , e ) => {
		e.stopPropagation();			// イベントの伝播を抑止 ****
		let val = nm ;
		if ( mess.innerText.trim() != '' ) {
			val = ' - ' + nm ;
		}
		mess.innerText += val ;
		console.log( nm ) ;
	}
	// クリアボタンがクリックされた処理
	const cldmess = ( ) => {
		mess.innerHTML = '&nbsp' ;
		console.log( '---' ) ;
	}
	// イベントリスナ登録
	p.addEventListener( 'click' , pdmess ) ;
	c.addEventListener( 'click' , cdmess ) ;
	gc.addEventListener( 'click' , gcdmess ) ;
	btn.addEventListener( 'click' , cldmess ) ;
</script>

それぞれの要素のイベント伝播が抑止されているので、入れ子になった要素でも、クリックが発火した要素のみイベントが配信されます。


既定の動作を抑止 Event.preventDefault()

イベントオブジェクトの preventDefault() メソッドは既定の動作を抑止します。

<html>
	<head>
		<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
		<style type="text/css">
			main { text-align: center; }
			#mess {
				margin: 0.5em 0 ;
				padding: 0.5em;
				border: solid 1px black;
			}
		</style>
	</head>
	<body>
		<main id="contents_id" class="contents_class">
			<h3>JavaScript 再入門</h3>
			<h4>既定の動作を抑止</h4>
			<div>
				<a id="a" href="https://sakura-system.com/">さくらシステムへのリンク</a>
			</div>
			<div id="mess">&nbsp;</div>
		</main>
	</body>
<script>
	const mess = document.querySelector('#mess') ;	// メッセージ表示要素を取得
	const a = document.querySelector('#a') ;	// リンクボタン要素を取得
	// リンクをクリック関数
	const dmess = ( e ) => {
		e.preventDefault();			// 既定の動作を抑止 ***
		let val = e.currentTarget.innerText ;
		mess.innerText = val + ' の動作は抑止されました。' ;
	}
	// イベントリスナ登録
	a.addEventListener( 'click' , dmess ) ;
</script>
</html>

<a> タグのリンクをクリックしたイベントで、既定の動作を抑止しています。
リンク先への遷移はされずに、メッセージが表示されています。


参考リンク

ウェブ開発を学ぶ > JavaScript > JavaScript の構成要素 > イベントへの入門
開発者向けのウェブ技術 > イベントリファレンス
開発者向けのウェブ技術 > Web API > Event
開発者向けのウェブ技術 > Web API > EventTarget > EventTarget.addEventListener()
開発者向けのウェブ技術 > Web API > EventTarget > EventTarget.removeEventListener()
開発者向けのウェブ技術 > Web API > AbortController
開発者向けのウェブ技術 > Web API > AbortSignal
開発者向けのウェブ技術 > Web API > AbortController > AbortController.abort()
開発者向けのウェブ技術 > Web API > Event > Event.stopPropagation()
開発者向けのウェブ技術 > Web API > Event > Event.preventDefault()