Javascriptのイベントフェーズを調べてみた件

Javascriptのイベントフェーズを調べてみた件

Javascriptのイベントは3つのフェーズにわかれています。
今回はこの3つのフェーズについて触れつつ、簡単な解説をしたいと思います。

3つのイベントフェーズとは

イベントフェーズは以下の3つに別れています。

  • キャプチャリングフェーズ
  • ターゲットフェーズ
  • バブリングフェーズ

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

 なんらかのイベントが発生した時にDOMツリーをたどってルート要素から発生要素を探しにフェーズです。Javascriptでのルート要素とは一般のhtmlを使ったwebサイトの場合、htmlではなくwindowオブジェクトを指します。

ターゲットフェーズ

発生要素でイベントが発火するフェーズです。
普段は無意識的にここで発火することを想定してコードを書いています。

バブリングフェーズ

ターゲットの要素から遡りルート要素までイベントを伝えます。キャプチャリングフェーズでたどってきた道をそのまま遡ります。ターゲットフェーズ以降の親要素のイベントは通常このタイミングで発火されます。

結局イベントフェーズとはどういうものなのか

文章だけではいまいち挙動がわからないのでサンプルを作成します。
モーダルを表示した際のクローズイベントをjs-modal-closeに登録します。
半透明の背景をクリックした際にモーダルが閉じるように設定していきます。

<!-- 省略 -->
<div class="p-eventphase">
  <div class="p-eventphase__bg js-modal js-modal-close">
    <div class="p-eventphase__bg__child js-modal-body">
      <p>contents text</p>
    </div>
  </div>
  <button class="p-eventphase__btn js-modal-open">modal</button>
</div>
<!-- 省略 -->
.p-eventphase {
  width: 100%;
  height: 50vh;
  position: relative;
  &__bg {
    display: none;
    background: rgba(#000, .5);
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1;
    &.is-open {
      display: flex;
      justify-content: center;
      align-items: center;
    }
    &__child {
      width: 50%;
      height: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
      background: #fff;
      border-radius: 20px;
      user-select: none;
    }
  }
  &__btn {
    width: 200px;
    height: 60px;
    text-align: center;
    background: #000;
    color: #fff;
  }
}
const modal = document.getElementsByClassName('js-modal')[0];
const modalContent = document.getElementsByClassName('js-modal-body')[0];
const open = document.getElementsByClassName('js-modal-open')[0];
const close = document.getElementsByClassName('js-modal-close')[0];

open.addEventListener('click', () => {
  modal.classList.add('is-open');
});

close.addEventListener('click', e => {
  console.log('parent');
  modal.classList.remove('is-open');
});

modalContent.addEventListener('click', e => {
  console.log('child');
});

この状態で展開されたモーダルの子要素、js-modal-bodyをクリックするとモーダルは閉じてしまいます。これではこちらの予期した挙動とは言えません。コンソールではchild、parentの順で表示されています。
何が起こっているのかというと、子要素のターゲットフェーズの後のバブリングフェーズで親要素のクリックイベントが発火しているのです。
次はバブリングを止めて親要素の発火を止めてみましょう。
modalContentに登録したイベントを以下のように書き換えます。

modalContent.addEventListener('click', e => {
  console.log('child');
  e.stopPropagation();
});

すると子要素をクリックしてもモーダルは閉じなくなります。
e.stopPropagation()によりバブリングを止めたからです。
コンソールではchildのみ表示されています。
ここまででなんとなくイベントを止めればいいんだということは伝わったかと思います。

最後にキャプチャリングフェーズとバブリングフェーズの違い確かめてみましょう。js-modal-closeに登録したjsを以下のように書き換えます。

close.addEventListener('click', e => {
  console.log('parent');
  modal.classList.remove('is-open');
}, {capture: true});

addEventListenerの第3引数に{capture: true}を追加しました。
captureはキャプチャリングフェーズでのイベントを発火させるオプションです。
子要素をクリックするとモーダルが消えコンソールにはparent、childの順に並んでいるかと思います。
これはターゲットフェーズより先にキャプチャリングフェーズで親要素のイベントが発火されているためです。
e.stopPropagation()はイベントのバブリングを止めるためのものなのでその前に発火するイベントに対しては効力を発揮できません。
captureオプションは慎重に使うことをおすすめいたします。

以上がJavascriptのイベントフェーズについての検証になります。
ご参考までに!

See you ~