uhyohyo.net

JavaScript初級者から中級者になろう

三章第四回 イベントキャプチャリング

このページの最終更新日:

いきなりですが、次のサンプルを見てください。


<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <div id="aaa">
      <p onclick="console.log('p');">test</p>
    </div>

    <script type="text/javascript">
      var div = document.getElementById('aaa');

      div.addEventListener('click',function(){
          console.log('div');
      }, true);

    </script>
  </body>
</html>

p要素にonclick属性がついています。

また、script要素内では、変数divにgetElementByIdでdiv要素を取得して代入し、そのaddEventListenerを呼び出しています。今までと違うのは3つ目の引数です。3つめの引数は、三章第二回で「とりあえずfalseにしておきましょう」といったものですが、trueになっています。これが今回の鍵です。

さて、このサンプルを実行してみます。onclick属性があり、またaddEventListenerでも「click」を登録しているので、とりあえずp要素をクリックしてみます。木構造は

div
  • p

ですね。ここで、前回解説したイベントバブリングの仕様に従って考えてみましょう。p要素をクリックすると、まずp要素でイベントが発生し、onclick属性で設定されたconsole.log('p');が実行されて「p」のログが表示されるでしょう。

その後、親要素であるdivに伝わり、divでイベントが発生します。divにはaddEventListenerで

function(){
    console.log('div');
}

が登録してあるので、それが実行されて「div」が表示されるはずです。

まとめると、「p」がまず表示されて、その後「div」が表示されると予想できます。

しかし、実際は違います。クリックしてみると、なんと先に「div」が表示されて、その後「p」が表示されます。ここに、今回の話題であるイベントキャプチャリングが関わってきます。

イベントキャプチャリングとは、イベントバブリングの前に、親要素から子要素に向けてイベントが伝わるというものです。

つまり、今回の場合、イベントバブリングで「p → div」という子から親の流れのに、「div → p」という親から子の流れがあったということです。

そして、実はaddEventListenerの3つめの引数は、そのイベントリスナ(関数)をイベントキャプチャリングで処理するならtrueイベントバブリングで処理するならfalseということになっています。ちなみに、イベント属性で登録されたイベント(今回のサンプルだとp要素のonclick属性で登録されたイベント)は、イベントバブリングで処理されることになっています。

より具体的に見ていくと、まず、

div
  • p ←ここ

でイベントが発生したとします。すると、イベントキャプチャリングの仕様にしたがって、まず

div ←ここ
  • p

から処理されます(もちろん、本当は一番上のhtml要素から順番に降りてきていますが、今回は省略しています)。ここで見られるのは、イベントキャプチャリングで処理するように登録された、つまりaddEventListenerの第三引数がtrueで登録されたものです。

今回、'div'をconsole.logする関数がこれにあたるので、ここでまず先に「div」が表示されたというわけです。

そのあと、親から子へイベントが伝わり、p要素が処理されます。しかし、p要素のイベント属性はイベントバブリングのほうで処理されるので、ここでp要素が処理するものはありません。よって、何も起こらず次に進みます。

ここからは前回解説したイベントバブリングです。まず

div
  • p ←ここ

が処理されます。上と同じp要素ですが、今度はイベントバブリングだから、イベント属性の処理が処理され、「p」が表示されます。その後、子から親へイベントが伝わりdiv要素が処理されます。しかし、div要素にはイベントバブリングで処理されるものはないので、何も起こらず、終了します。これが一連の流れです。

さて、ここで、上のサンプルを


<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <div id="aaa" onclick="console.log('div2');">
      <p onclick="console.log('p');">test</p>
    </div>

    <script type="text/javascript">
      var div = document.getElementById('aaa');

      div.addEventListener('click',function(){
        console.log('div');
      },true);
    </script>
  </body>
</html>

のようにしてみたら、どうなるか分かりますか? そう、div要素についたイベント属性はバブリングフェーズで処理されるから、p要素の「p」よりも後、つまり、「div」「p」「div2」の順番でログが表示されます。

フェーズ

さて、いまイベント処理の一連の処理をみてきましたが、実はこの流れは3つの段階に分けることができます。そのひとつひとつはフェーズと呼ばれます。

まず、イベントが発生して、イベントキャプチャリングの仕様にそって親から子へイベントを見ていく処理があります。この処理は、キャプチャリングフェーズといいます。

その後、発生源の要素に到達し、その要素の処理を行います。これは今までイベントバブリングに含めていましたが、厳密にはここだけを抜き出してターゲットフェーズといいます。

そして、イベントバブリングの仕様に従って、発生源から親へイベントを見ていく段階がバブリングフェーズです。

この3段階になっています。この一連の流れを、全部あわせてイベントフローといいます。

今回はイベントキャプチャリングを紹介しましたが、使う機会はそれほど多くありません。ほとんどの場合はイベントバブリングだけで事足ります。イベントキャプチャリングは、イベントバブリングで処理される本処理の前にちょっと準備処理のようなものが必要だという場合に使われることがあるようです。

addEventListenerの第3引数について

今回みたように、addEventListener(やremoveEventListener)の第3引数は、イベントをキャプチャリングフェーズに登録するならtrue、バブリングフェーズに登録するならfalseということでした。

しかし、これは実は少し古い書き方です。より最近の書き方(具体的にはいわゆるDOM4)では、次のようにオブジェクトで指定することができます。


div.addEventListener('click', function(){
  console.log('div');
}, {
  capture: true
});

captureプロパティを持つオブジェクトを第3引数として渡すことで、そのプロパティがtrueかfalseかで登録フェーズを変えることができます。

ただし、この書き方は、新しい書き方のためまだ対応ブラウザに不安がある(古いブラウザでは動かない可能性がある)ので、現時点(2017年7月現在)では使用は避けたほうがいいかもしれません。

このように真偽値ではなくオブジェクトで指定する書き方が新しく登場した理由は、このオブジェクトにcapture以外のオプションも設定できるようにするためです。具体的には、passiveおよびonceというオプションを設定できます。この詳細はここでは扱いませんので、気になる人は調べてみてください。

最後になりますが、実はaddEventListenerの第3引数は省略できます(false扱いになります)。