ぷるぷるの雑記

低レイヤーがんばるぞいなブログ. 記事のご利用は自己責任で.

JavaScriptのイベントの伝搬とターゲット要素

JavaScriptのイベントの伝搬(バブリングとキャプチャリング)とターゲット要素について実験をしながら理解を深めていきましょう. コーディングで楽をするためにVueを使いますが、シュガーシンタックスを使うだけなので生のJavaScriptでも同じ結果になります.

HTMLの階層構造

HTMLは階層構造になっていて、全ての要素は最終的にWindowのdocument要素を祖先に持ちます. そんなことを知らなくても、HTMLを書いていれば自然と階層構造を持つことになるでしょう. 例えば、以下のように3世代すべてdiv要素であるHTMLがあるとします.

<!-- index1.html -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">
    <div class="first">
        First
       <div class="second">
            Second
            <div class="third">
                Third
            </div>
       </div>
    </div>
</div>

<style>
.first{
    background-color: blue;
    padding: 30px;
    font-size: 30px;
    cursor: pointer;
}
.second{
    background-color: rgb(111, 111, 255);
    padding: 30px;
}
.third{
    padding: 30px;
    background-color: rgb(182, 182, 255);
}
</style>

divの親子孫階層

この階層をWindowから図式すると以下のようになります.

すべての階層構造

この階層構造はあくまでDOMツリー上の階層構造のことであり、表示上の階層構造とは関係ありません.

イベントの伝搬

この時、親子孫要素をクリックすると挙動にどのような変化があるでしょうか. 実はJavaScriptではイベントは自然と伝搬するようにできています. このイベントの伝搬には向きがあり、親→子→孫(降順)と孫→子→親(昇順)のものがあります. 降順でのイベントの伝搬をキャプチャリング、昇順でのイベントの伝搬をバブリング といいます.

キャプチャリングとバブリングは共通して以下のようなポイントがあります

  1. イベントを受け取った要素で折り返すようにイベントは伝搬する
  2. 伝搬の道中で対応したイベントリスナが登録されていれば実行される
  3. 伝搬の道中でイベントリスナが実行されてもイベントの伝搬は自動的には止まらない
  4. どの要素のイベントリスナであろうと、e.targetは折り返しになった要素になる

例えば、index1.htmlで孫要素をクリックすると、次のようにイベントの伝搬が起こります.

孫要素をクリックした場合のイベント伝搬のようす

親要素をクリックした場合、伝搬の折り返し地点とe.targetが指す要素が次のようになります.

親要素をクリックした場合のイベント伝搬のようす

イベントリスナ

次にイベントリスナが登録されていた場合のイベントの伝搬を見てみましょう. 次のhtmlとjsのように、クリックイベントリスナを登録して親子孫要素をクリックしたときの出力の違いを見てみます.

<!-- index2.html -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">
    <div class="first" 
    @click="onClickFirst($event)"
    >
        First
       <div class="second"
       @click="onClickSecond($event)"
       >
            Second
            <div class="third"
            @click="onClickThird($event)"
            >
                Third
            </div>
       </div>
    </div>
</div>

<script type="module" src="js/main1.js">
</script>

<style>
.first{
    background-color: blue;
    padding: 30px;
    font-size: 30px;
    cursor: pointer;
}
.second{
    background-color: rgb(111, 111, 255);
    padding: 30px;
}
.third{
    padding: 30px;
    background-color: rgb(182, 182, 255);
}
</style>
// main1.js

const { createApp } = Vue

createApp({
data() {
    return {
    message: 'Hello Vue!',

    }

},
methods:{
    onClickFirst(e){
        console.log('First div clicked')
        console.log(e.target)
    },
    onClickSecond(e){
        console.log('Second div clicked')
        console.log(e.target)
    },
    onClickThird(e){
        console.log('Third div clicked')
        console.log(e.target)
    },
}
}).mount('#app')

親要素をクリックした場合

子要素をクリックした場合

孫要素をクリックした場合

注目したいのは、折り返しの要素が深いとその祖先のイベントリスナも実行されていることと、クリックイベント単位で見るとe.targetが指す要素が常に折り返しの要素であるということですね. この結果を見るだけでイベントの伝搬の仕組みとe.targetが指す要素が理解が深まりますね. ちなみに、コンソールに出力される順番からも分かるように、Vueの@click="Mufunc"というシュガーシンタックスはバブリング時のイベントリスナを登録します.

さて、e.targetが常に折り返しの要素を表すというのではかゆいところに手が届かない感がありますね. おそらく、呼び出されたイベントリスナをもつ要素を取得したいことが多いのではないでしょうか. そんなときのためにJavaScriptには Event.currentTarget が用意されています. これはイベント伝搬中の現在の要素を表します. 先ほどのスクリプトの中のe.targetをe.currentTargetにすると以下のような出力になります.

親要素をクリックした場合のcurrentTarget

子要素をクリックした場合のcurrentTarget

孫要素をクリックした場合のcurrentTarget

無事イベントリスナごとの要素を得ることが出来ました. これの亜種のようなものとして一部のクラスは relatedTarget というインスタンスを持ちます. これはクラスに応じた副ターゲットを表します. 例えば、MouseEventやこれを継承したDragEventの場合、副ターゲットとは前回のターゲットを表します. 副ターゲットを調べることでLeaveイベントリスナを想定通りの動きにすることが出来たりします. 使い道は限られますが、だからこそ重要な場面で使われるイメージです. ターゲット要素についてまとめると以下のようになります.

ターゲット要素 すべてのイベントが持つか 説明
Event.target Yes イベント単位で常に同じものを指す
Event.currentTarget Yes イベント伝搬中の現在の要素を指す
SomeEvent.relatedTarget No クラスに応じた副ターゲットを指す

キャプチャリングとバブリング

普段あまり意識することはないと思いますが、降順にイベントが伝搬していくフェーズをキャプチャリングといいます. 反対に、昇順にイベントが伝搬していくいわゆるなフェーズをバブリングと言います. 次のような特徴があります.

  1. Vueでは @click.capture="MyFunc" のように記述することでキャプチャリング時のイベントリスナを登録する
  2. Vueでは @click="MyFunc" のように記述することでバブリング時のイベントリスナを登録する
  3. キャプチャリング時のイベントリスナであっても、 Event.targetはイベント伝搬の折り返しの要素を指す
  4. イベントの伝搬を止めたい場合 @click.capture.stop="MyFuncOnlyParent" @click.stop="MyFuncOnlyChild" のように記述する
  5. キャプチャリング時のイベントの伝搬を止めた場合バブリング時のイベント伝搬も止まる

以下のようなhtmlとjsで孫要素をクリックすると得られる出力を見ればこれらの特徴を確認することが出来ます.

<!-- index2.html -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">
    <div class="first" 
    @click="onClickFirst($event)"
    @click.capture="onClickFirst($event)"
    >
        First
       <div class="second"
       @click="onClickSecond($event)"
       @click.capture.stop="onClickSecond($event)"
       >
            Second
            <div class="third"
            @click="onClickThird($event)"
            @click.capture="onClickThird($event)"
            >
                Third
            </div>
       </div>
    </div>
</div>

<script type="module" src="js/main2.js">
</script>

<style>
.first{
    background-color: blue;
    padding: 30px;
    font-size: 30px;
    cursor: pointer;
}
.second{
    background-color: rgb(111, 111, 255);
    padding: 30px;
}
.third{
    padding: 30px;
    background-color: rgb(182, 182, 255);
}
</style>
// main2.js

const { createApp } = Vue

createApp({
data() {
    return {
    message: 'Hello Vue!',

    }

},
methods:{
    onClickFirst(e){
        console.log('First div clicked')
        console.log(e.target)
    },
    onClickSecond(e){
        console.log('Second div clicked')
        console.log(e.target)
    },
    onClickThird(e){
        console.log('Third div clicked')
        console.log(e.target)
    },
}
}).mount('#app')

キャプチャリングとバブリングの特徴の確かめ

参考

ja.javascript.info

johobase.com

developer.mozilla.org

developer.mozilla.org

developer.mozilla.org