【JavaScript】 addEventListener()の第三引数useCaptureの謎
更新日:2023/01/16
JavaScriptでDOM要素を操作する場合、addEventListener()メソッドを使用します。
このメソッドの第三引数にtrueをセットしているコードをよく目にします。
しかしtrueでもfalseでも同じように動作するので、どんな意味があるのかわからない人も多いのではないでしょうか。
そこで今回は、addEventListener()メソッドの第三引数についてお伝えします。
useCaptureの意味
addEventListener関数は次のような構文です。
addEventListenerの構文
addEventListener(type, listener, useCapture)
useCaptureは3番目の引数で、trueまたはfalseの真偽値で指定します。
このuseCaptureは2番目の引数の listenerをコールバック関数を、キャプチャフェーズで呼び出すかどうかを意味しています。
フェーズにはキャプチャフェーズと、バブリングフェーズの2種類あります。
要素をクリックすると、最上位のwindowから子要素に向かって次の条件に合う要素を検索します。
addEventListener関数を実行していて、useCaptureがtrue
条件に合っていたら、コールバック関数が呼び出されます。
この流れが、キャプチャフェーズです。
子要素にたどり着くと、次は最上位に向かって次の条件に合う要素を検索します。
addEventListener関数を実行していて、useCaptureがfalse
条件に合っていたら、コールバック関数が呼び出されます。
この流れが、バブリングフェーズです。
この流れからわかるように、クリックされた要素以外の要素でもイベントが発生します。
その際に、キャプチャフェーズでイベントを処理するかどうかで、イベントが発生する順番が変わってきます
これだけではわかりにくいので、順を追って解説します。
useCaptureの意味がないケース
次のようなhtmlでクリックイベントを捕捉してみます。
html
<div id="div1" >
<div id="div2" >
<div id="div3" style="width:100px;height:100px;border:1px solid #555">
</div>
</div>
</div>
上のhtmlをブラウザで表示すると、下のような3つのdiv要素が重なり合った図形になります。
JavaScriptで、div3にクリックイベントを登録します。
JavaScript
window.addEventListener( "DOMContentLoaded" , ()=> {
const click = e =>{
console.log( "click:" + e.currentTarget.id );
}
document.getElementById("div3").addEventListener("click",click);
});
addEventListenerの第三引数が指定されていないので、デフォルトとしてfalseが有効になります。
falseはバブリングフェーズなので前項(useCaptureの意味)を読むと、クリックされたdiv3からdiv2、div1とイベントが発生すると受け取る人がいるかもしれません。
実行結果は次のようになります。
実行結果
click:div3
div3で一度イベントを捕捉するのみです。
では、addEventListenerの第三引数useCaptureをtrueにしてみます。
addEventListenerの第三引数useCaptureをtrueに変更
document.getElementById("div3").addEventListener("click",click, true );
キャプチャフェーズでのイベント通知が有効になったので、windowからdiv3まで順番に通知されるのでしょうか?
実行結果は次のようになります。
実行結果
click:div3
先ほどと同じです。
つまり、今回の例ではuseCaptureに何を指定しても、プログラムの動作に影響がありません。
またaddEventListenerは、登録した要素のみにイベントを通知することも重要なので、覚えておいてください。
useCaptureの意味があるケース
ではaddEventListenerの第三引数useCaptureは、どんなときに使い分ければいいのでしょうか。
それは階層が重なり合う要素に対して、各々同じイベントを登録するときです。
先ほどのhtmlはそのままで、JavaScriptを変更してみます。
重なり合う要素に同じイベントを登録
window.addEventListener( "DOMContentLoaded" , ()=> {
const click = e =>{
console.log( `click:currentTarget[${ e.currentTarget.id}] target[${ e.target.id}] phase[${ e.eventPhase}]` );
}
document.getElementById("div3").addEventListener("click",click);
document.getElementById("div2").addEventListener("click",click);
});
div3の下にあるdiv2にも、クリックイベントを登録しました。
ブラウザで表示して、div3をクリックすると次のような結果となります。
実行結果
click:currentTarget[div3] target[div3] phase[2]
click:currentTarget[div2] target[div3] phase[3]
useCaptureはfalseなので、クリックされたdiv3からdiv2へとイベントが伝播しています。
ちなみにcurrentTargetは今処理している要素を指し、targetはクリックされた要素です。
関連記事:【JavaScript】 targetとcurrentTargetの覚書
またphaseは、1がキャプチャフェーズ、3がバブリングフェーズです。
ここまで触れていませんでしたが、ターゲットとなる要素が見つかるとキャプチャフェーズからターゲットフェーズに切り替わります。
phase[2]は、ターゲットフェーズです。
では、addEventListenerの第三引数useCaptureをtrueにしてみます。
addEventListenerの第三引数useCaptureをtrueに変更
document.getElementById("div3").addEventListener("click",click,true);
document.getElementById("div2").addEventListener("click",click,true);
ブラウザで表示して、div3をクリックすると次のような結果となります。
実行結果
click:currentTarget[div2] target[div3] phase[1]
click:currentTarget[div3] target[div3] phase[2]
キャプチャフェーズでのイベント通知が有効になったので、下層のdiv2からdiv3の順番でイベントが発生しています。
今回の例ではdiv2とdiv3が完全に重なり合っているためdiv3のみクリックできます。
しかし大きさを変えdiv2もクリックできるようにして、それぞれ異なる処理をおこなうケースの方が多いはずです。
その際に、どのような順番でイベントを発生させるのかを、自分で適切に制御する必要があります。
useCaptureの使い分け
ここまでuseCaptureが、キャプチャフェーズとバブリングフェーズを制御しているととれる書き方をしてきました。
しかし実際には、DOMをたどってイベント要素を探す作業(キャプチャフェーズ)中にイベントを通知するか、要素にたどり着いた後ルートへと遡る作業(バブリングフェーズ)中にイベントを通知するかのフラグでしかありません。
これまでの例で、イベント登録を次のようにおこなってみます。
useCaptureの使い分け
document.getElementById("div3").addEventListener("click",click);
document.getElementById("div2").addEventListener("click",click,true);
document.getElementById("div1").addEventListener("click",click);
div1、div2、div3にそれぞれクリックイベントを登録して、div2のみuseCaptureにtrueを指定しています。
ブラウザ表示後、div3をクリックすると次のような結果になります。
実行結果
click:currentTarget[div2] target[div3] phase[1]
click:currentTarget[div3] target[div3] phase[2]
click:currentTarget[div1] target[div3] phase[3]
div2→div3→div1の順番でイベントが発生しました。
これは次の図のような流れの中で、各要素でuseCaptureを見てイベント通知を判断しているからです。
なお同じ要素に対して、trueとfalse両方のイベントを登録することも可能です。
trueとfalse両方のイベントを登録
document.getElementById("div3").addEventListener("click",click);
document.getElementById("div2").addEventListener("click",click);
document.getElementById("div2").addEventListener("click",click,true);
document.getElementById("div1").addEventListener("click",click);
実行結果
click:currentTarget[div2] target[div3] phase[1]
click:currentTarget[div3] target[div3] phase[2]
click:currentTarget[div2] target[div3] phase[3]
click:currentTarget[div1] target[div3] phase[3]
例題
次のDOM要素に対してクリックイベントを、div2→div4→div5→div5→div3→div2→div1の順で発生させます。
html
<div id="div1" >
<div id="div2" >
<div id="div3" >
<div id="div4" >
<div id="div5" style="width:100px;height:100px;border:1px solid #555">
</div>
</div>
</div>
</div>
</div>
回答
まず発生させたい順番にaddEventListenerを記述します。
順番にaddEventListenerを記述
div2.addEventListener("click",click);
div4.addEventListener("click",click);
div5.addEventListener("click",click);
div5.addEventListener("click",click);
div3.addEventListener("click",click);
div2.addEventListener("click",click);
div1.addEventListener("click",click);
あらかじめdiv1などにはgetElementById()などで要素をセットしてあるとします。
クリックされる要素、ここではdiv5以前のaddEventListener()の第三引数useCaptureにtrueを指定します。
第三引数useCaptureにtrueを指定
div2.addEventListener("click",click,true);
div4.addEventListener("click",click,true);
div5.addEventListener("click",click,true);
div5.addEventListener("click",click);
div3.addEventListener("click",click);
div2.addEventListener("click",click);
div1.addEventListener("click",click);
div5はイベントが二つ登録されているので、どちらかをtrueにする必要があります。
この状態でdiv5をクリックすると、順番にイベントが発生します。
useCaptureをオブジェクトで指定
addEventListenerの第三引数は真偽値の他に、複数のオプションをオブジェクトを受け付けます。
オブジェクトは、次の決まった名称のプロパティをセットします。
options:{ capture : 真偽値 once : 真偽値 passive : 真偽値 }
■options.capture
useCaptureで指定した真偽値と同じ意味を持ちます。
■options.once
イベント発生後に、イベントリスナーが削除されます。
つまり、一回のみイベントが発生します。
■options.passive
イベントの規定の動作(リンク遷移やチェックボックスの切り替え等)をキャンセルする、preventDefault()が使用可能かどうかを指定します。
この値がtrueの場合、イベント内でpreventDefault()を呼び出しても無効です。
基本的にはこのオプションのデフォルトはfalseですが、スマホのタッチイベントなど一部のイベントはtrueになるようです。
preventDefault()が効かないときは、このオプションをfalseにセットしてみてください。
removeEventListener() とuseCapture
addEventListener()は同一の要素に対して、useCaptureが異なれば同じ関数を登録することができます。
そのため、addEventListener() で登録したイベントをremoveEventListener()で削除するときは、useCaptureを一致させる必要があります。
removeEventListener() はuseCaptureを一致させる
document.getElementById("div2").addEventListener("click",click,true);
// useCapture=trueで登録されているため、削除できない
document.getElementById("div2").removeEventListener("click",click);
// 削除可能
document.getElementById("div2").removeEventListener("click",click,true);
オプションはcaptureプロパティのみチェックされます。
addEventListener()は関係ないイベントも伝播する
例えば次のようなラジオボタンがあるとします。
html
<div id="testarea">
<label><input type="radio" name="test" value="1">Value:1</label>
<label><input type="radio" name="test" value="2">Value:2</label>
<label><input type="radio" name="test" value="3">Value:3</label>
</div>
このときdiv要素にchangeイベントを登録します。
JavaScript
document.getElementById("testarea").addEventListener("change",e =>{
if( e.target.type !== "radio" ) return;
console.log( `click:name[${e.target.name}] value[${e.target.value}]` );
} );
div要素にはchangeイベントはありません。
しかしラジオボタンで発生したchangeイベントがフェーズで伝播して、div要素に通知されます。
このとき後から動的に選択肢を追加したとしても、div要素でイベントを捕捉できます。
■ラジオボタンにchangeイベントを登録した場合
// 全ての選択肢にイベントを登録する document.getElementsByName("test") .forEach( e => e.addEventListener("change",e =>func() ) );
動的に選択肢を追加したら、その選択肢にchangeイベントを登録する必要があります。
更新日:2023/01/16
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。