addEventListener の第3引数
2021-10-13
こんにちは
どうも、僕です。
addEventListener の第3引数って知ってますか。僕は知りませんでした。いや、厳密に言うと、存在は知ってたけど理解してませんでした。
理解するために、DOMのイベントフローとともに見ていきたいと思います。
イベントフローとは
まずはイベントフローとは何かについて考えます。
イベントフローとは、DOM に対するイベントの委任(伝播)のことで、例えば以下のようなコードが動くのはイベントフローによるものです。
これは、parent をクリックすると当然 `parent clicked` と表示されますが、child の方をクリックしても同じように `parent clicked` と表示されます。
parent here
child here
では、逆の場合はどうでしょう?
クリックイベントを parent から child にしました。これで parent をクリックしても何も表示されません。当然だと思う人もいるかもしれませんが、これはイベントキャプチャと呼ばれるフローによるもので、このフローや実行について制御することができるのが、addEventListener の第3引数ということです。
parent here
child here
イベントフェーズ
まず、イベントにはキャプチャとバブリングというものがあります。
簡単にいうと、キャプチャは上から下への伝播、バブリングは下から上への伝播です。
イベントは、キャプチャをしてからバブリングをしてDOM全体に浸透します。伝播中のイベントはEvent.stopPropagation() で中断することも可能です。
また、上であげたイベントフローについてですが、以下の3つのフェーズを経て伝播されます。
キャプチャフェーズはキャプチャを、バブルフェーズはバブリングをすることがわかります。ターゲットフェーズはトリガーとなる要素のことで、これらをつなぎ合わせてくれる役目を持っています。
それぞれについて詳しく見ていきます。
キャプチャフェーズ
キャプチャフェーズは最初の段階です。
Global object(window) から、ドキュメントツリーをたどって、ターゲットノードまでイベントが伝播する段階です。
このフェーズで登録されたイベントリスナー(一番下の要素までなぞってない場合)は、ターゲットまで到達する前に処理されます。さっきの例だと、parent はターゲットではないですが、イベントが処理されて下の要素にまで伝播されたので、ここでいうキャプチャフェーズで捕捉されたイベントに該当します。
ターゲットフェーズ
ターゲットフェーズは、イベントがターゲットに到達した時に処理されるイベントのことです。ターゲットに到達すると、トリガーとなってイベントが発火します。
例えば、以下のような DOM があるとします。
hello world
このような時、イベントは html -> body -> div -> p の順でなぞられます。p はターゲットです。eventListener はターゲットに到達するとイベントが発火します。
バブルフェーズ
バブルフェーズでは、イベントがバブリングするフェーズです。ターゲットフェーズでは、p タグに到達するまででしたが、その後 p -> div -> body -> html といったように下から上になぞっていく状態です。
ほとんどのイベントはこのバブリングが行われますが、以下のイベントについてはバブリングをしません。
バブルしないイベントについては、ターゲットフェーズで処理が止まります。
イベントフローの制御と addEventListener
DOM Level 2のイベントについては、addEventListener で登録することができます。
余談ですが、addEventListener は React で state を絡めて使用する際には注意が必要です。おそらく、useEffect で eventListener を登録すると思うのですが、それだと初期ロード時点で登録されたイベントになってしまい、state が最新の値に保たれません。そのような時は useRef を使うか、useEffect の第二引数で依存を適切に渡すことで mount されるたびにイベントの登録をし直すことが必要になります。
話が逸れましたが、[addEventListener](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener) についてです。
シンタックスについては
target.addEventListener(type, listener [, options]);
target.addEventListener(type, listener [, useCapture]);
となっています。
それぞれの引数について見ていきます。
options
options については、以下の3つの値を持たせることができ、listener の制御を行うことができます。(非推奨については省略)
capture
capture は、DOM ツリーでターゲットまで配信される前に、登録された listener に配信されます。
つまり、本来下から上にバブリングされるイベントを、キャプチャの時点で受け取り、実行してくれます。
useCapture を true にするのと同じです。
once
once は、そのイベントが1回きりしか呼ばれないようにします。
例えば、以下のようにすると、最初の1回は console に hoge が出力されますが、2回目以降は何も表示されなくなります。
hoge
passive
passive は、listener が指定された関数が [preventDefault()](https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault) を呼ばないことを明示します。
呼び出された listener が preventDefault を呼び出しても、何も表示されません。warning が出ます。
useCapture
useCaputure については、boolean の値で、DOM ツリー内の下のイベントターゲットに配信される前に、登録された listener に配信されるかどうかを示します。
デフォルトでは false に設定されていて、これを true にするとバブリングの際に捕捉するイベントを先に捕捉するようになります。例えば、以下のようなコードがある時、piyo をクリックすると下から上にバブリングするようにイベントが拾われ、console には piyo、fuga、hoge の順で表示されます。
hoge
fuga
piyo
// piyo をクリックした時の出力
piyo
fuga
hoge
では、これの fuga の部分の addEventListener に true を設定するとどうでしょうか?
この状態で piyo をクリックすると fuga、piyo、hoge の順で出力されます。true にした要素が先に捕捉されるようになりました。
hoge
fuga
piyo
// piyo をクリックした時の出力
fuga
piyo
hoge
まとめ
addEventListener の第3引数についてこれまであまり考えたことがなかったのでいい学びになりました。また、DOM とまだまだ仲良くなれないなと感じました。
最近だと命令的にイベントを宣言する機会も減って、根本理解をする機会が自分自身の中で減ってる気がするので、どこまでもネイティブを追い求められるように頑張っていきたいなと思います。