JavaScriptのイベントの伝播について【キャプチャリングとバブリング】

2021年2月25日JavaScript

JavaScriptのイベントの伝播(Event Propagation)についてまとめました。

イベントの伝播(Event Propagation)

子要素にクリックイベントがある場合、親をたどってどんどん伝播していくという仕組みがあります。
すべてのイベントが伝播するわけではありませんが、イベントの伝播の仕組みを理解しておきましょう。

たとえば下のように6つのliタグ全部イベントを追加したい場合、親要素であるulタグ一つにイベントを追加しておけばいいので便利になります。

たとえば、クリックしたものを赤色に変えるイベントを作ったとしましょう。

  • その場合、クリックした要素はe.target
  • eventListnerを追加した要素は、e.currentTargetで取得できます。

まずはさらっとどんなことができるのかをみてみましょう。

下の例では、evenlistenerがあるのはulで、e.targetであるliがクリックされてもredクラスのつけ外しができるプログラムです。

  <style> li.red { color: red; } </style>
    <section>
      <ul><!-- e.currentTarget -->
        <li>1つ目のliタグです。</li><!-- e.target -->
        <li>2つ目のliタグです。</li><!-- e.target -->
        <li>3つ目のliタグです。</li><!-- e.target -->
        <li>4つ目のliタグです。</li><!-- e.target -->
        <li>5つ目のliタグです。</li><!-- e.target -->
        <li>6つ目のliタグです。</li><!-- e.target -->
      </ul>
    </section>

イベントの伝播の流れ:イベントフェーズ

イベントの伝播をしっかりと理解するには、イベントの段階(イベントフェーズ)を理解する必要があります。
イベントフェーズは次の3つの段階で発生します。

  1. キャプチャリングフェーズ (Capturing phase)
  2. ターゲットフェーズ(Target Phase)
  3. バブリングフェーズ(Bubbling Phase)

キャプチャフェーズは、DOMツリーをたどってルート要素から発生要素を探しに行きます。
ターゲットフェーズは、発生要素を検出します。
バブリングフェーズは、ルート要素まで遡ります。

詳しくそれぞれをみてみましょう。

DOMツリーの構造

上の場合のDOMツリーの構造は次のようになっています。

  • DOCUMENT
  • ELEMENT(html)
  • ELEMENT(body)
  • ELEMENT(section)
  • ELEMENT(ul)
  • ELEMENT(li)

キャプチャリングフェーズ Capturing phase

ここで、たとえばクリックのイベントがliタグに設定されているとします。
すると、DOCUMENTルートという一番上でイベントを捉えます。(キャプチャリングフェーズ Capturing phase)
そこから、html→body→…liタグというようにどんどんDOMツリーを渡ってクリックイベントがパスされていきます。

ターゲットフェーズ Target phase

そして、liタグに渡ったら即座にイベントが実行されます(ターゲットフェーズ Target phase)
イベントが実行されるというのは、querySelector('a’) と選択されていてるとすると、それのaddEventlistener内のコールバック関数が実行されるということです。

バブリングフェーズ Bubbling phase

そして、イベントが反応すると、次はDOCUMENTルートへ向かってどんどん上に上がっていきます。(バブリングフェーズ Bubbling phase)
バブリングフェーズでは、キャプチャリングフェーズの逆でliタグ→pタグ→…DOCUMENTルートとクリックイベントが渡されていきます。

このようなイベントの伝わっていく仕組みのことをevent propagation(イベント伝播)といいます。

フェーズは操作できる

イベントはキャプチャリングフェーズとバブリングフェーズでハンドルすることができますが、それを変更することが可能です。

そしてすべてのイベントがキャプチャリングとバブリングが起こるわけではありません。

いくつかのタイプではターゲットイベントでそのままハンドルされるものもあり、そこで実行されます。

ただ、ほどんどのイベントが上のキャプチャリングとバブリングがあるということを知っておきましょう。

そして、これらのフェーズは手動で操作して、イベントの伝播を制御することができます。

addEventListenerの第三引数:イベントの伝播を制御

イベントの伝播を制御するには、addEventListenerの第三引数を設定します。

addEventListenerの第三引数は、true/falseのどちらかになります。

  • trueの場合、キャプチャーフェーズで発生要素を探しに行きます。
  • falseの場合、バブリングフェーズで要素を探しに行きます。
  • 初期値は、falseになっています。

stopPropagation()メソッド

なお、イベントの伝播はstopPropagation()メソッドを呼び出すことで止めることができます。

しかし、イベントの伝播は基本的に起こることを前提としているため、他の問題を引き起こす可能性があります。(Propagation=伝播)
そのため、stopPropagation()メソッドの使用は極力避けて、その代わりにイベンド自体を書き換えられないか検討しましょう。

イベント伝搬の具体例

わかりやすいように、ランダムに色が変わるクリックイベントをaタグからulタグ、navタグにまで伝搬させます。

HTML
<nav class="nav">
<ul class="nav__links">
  <li class="nav__item">
    <a class="nav__link" href="#">HOME</a>
  </li>
  <li class="nav__item">
    <a class="nav__link" href="#">ABOUT</a>
  </li>
  <li class="nav__item">
    <a class="nav__link" href="#">WORKS</a>
  </li>
</ul>
</nav>
JavaScript


//ランダムな配色を定義
  //rgb(0-100,0-100%,0-100%), randomInt(min, max)
  const randomInt = (min, max) =>
  Math.floor(Math.random() * (max - min + 1) + min);
  
  const randomColor = () =>
  `hsl(${randomInt(0, 100)}, ${randomInt(0, 100)}%, 50%)`;

//aタグにクリックイベントを設定
  document.querySelector('.nav__link').addEventListener('click', function (e) {
  //このthisはイベントハンドラーがついているものを意味します。この場合は'.nav__link'です。
  this.style.backgroundColor = randomColor();
  console.log('aタグ', e.target, e.currentTarget);
  //aタグ,e.targetはこのイベントがついているaタグ、e.currentTargetも同じ


//イベントの伝搬を止めたい場合
  // e.stopPropagation();
});

//ulタグにクリックイベントを設定
  document.querySelector('.nav__links').addEventListener('click', function (e) {
  //このthisは'.nav__links'
  this.style.backgroundColor = randomColor();
  console.log('ulタグ', e.target, e.currentTarget);
  //ulタグと下の階層のaタグイベントがついているaタグ
  //e.currentTargetも同じはulタグ === thisと同じ
});

//navタグにクリックイベントを設定
  document.querySelector('.nav').addEventListener(
  'click',
  function (e) {
    //このthisは'.nav'
    this.style.backgroundColor = randomColor();
    console.log('navタグ', e.target, e.currentTarget);
    //nacタグと下の階層のaタグイベントがついているaタグ
    //e.currentTargetも同じはnavタグ === thisと同じ
  },
//ここが第三引数。trueでキャプチャリングフェーズに変更できる。
  true
);

バブリングの具体例

この場合、ulタグのaタグにclickイベントがついています。aタグをクリックするとバブリングする(bubbling up)のでulタグの全体もaタグがクリックされるたびに変化します。
しかし、ulタグをクリックしてもaタグは変化しません。なぜなら、バブリングは下の階層にはいかないからです。
e.targetのtargetはイベントの発生源を意味します。つまり、この場合はaタグで起こっていることを意味します。バブリングで反応しているulタグも、navタグもすべてイベントの発生源はaタグで表示されます。つまり上記のイベントをあらわすeには、すべて全く同じイベントが格納されていることを意味します。
また、e.currentTargetはthisと同じものを指します。

イベント伝搬を止める方法:stopPropagation()

e.stopPropagation()とします。
ただし、イベント伝搬を止めるのは他に不具合を起こす可能性があるためあまり推奨できません。

第三引数trueでキャプチャリングフェーズ

イベントハンドラーはバブリングに反応しますが、第三引数をデフォルトのfalseからtrueに変更することでキャプチャリングフェーズで反応することができます。

trueにするとキャプチャリングフェーズでイベントを実行するため、階層が上のものから順番に実行されていきます。
true/falseを入れ替えてみて、反応を確認してみましょう。

イベント移譲(Event delegation)

イベント移譲(Event delegation)は、イベントを起こしたい要素の共通の親要素にイベントを設定してバブリングで適用できるようにすることです。
ステップとして2ステップあります。

  • イベントを起こしたい要素の共通の親要素にイベントを設定する
  • イベントの発生となる要素を条件指定する

以下に、イベント移譲を使わないやり方とイベント移譲を使ったやり方の両方を記載しますので、見比べてみましょう。

(HTMLは上と同じものを使用)



// *** イベント移譲を使わないやり方 *** //
document.querySelectorAll('.nav__link').forEach(function (el) {
  el.addEventListener('click', function (e) {
    e.preventDefault(); //移動しなくなる
    const id = this.getAttribute('href'); //this === .nav__link、それのhrefをgetする
    console.log(id); // #section--1とかがgetできる。

    document.querySelector(id).scrollIntoView({behavior: 'smooth'});
  });
});

// *** イベント移譲(Event delegation) *** //
//イベントを起こしたい要素の共通の親要素にイベントを設定する
  document.querySelector('.nav__links').addEventListener('click', function (e) {
    e.preventDefault(); //移動しなくなる
    console.log(e.target);

// イベントの発生となる要素を条件指定する
    if (e.target.classList.contains('nav__link')) {
      const id = e.target.getAttribute('href'); //thisは.nav-linksになってしまうので使えない。e.targetを使う
      console.log(id); // #section--1とかがgetできる。
      document.querySelector(id).scrollIntoView({ behavior: 'smooth' });
    }
});

イベント移譲を使わないやり方は、document.querySelectorAll('.nav__link’)でNodeListを取得し、それをforEachでそれぞれ実行するというやり方です。
イベント移譲(Event delegation)を使うやり方の方が効率的になります。
イベントの発生となる要素を条件指定する方法は、今回はcontains()で該当のクラス名があったらとしています。