DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017


こんにちは、ほそ道です。

今回はDOMネタです。
イベントのキャプチャとバブリングについて覚書をまとめて参ります。

また今回はv8での検証であり、レガシーなIEは対象外です。
レガシーなIEはイベント設定メソッド自体が違いますのでご注意くださいませ。

目次はこちら

入れ子なDOMのイベント発生順序制御

DOMが入れ子構造になっていてそれぞれにイベント(例えばClickイベント)が設定されていた場合
「このように動いてほしい」という期待はケースバイケースであると思います。
期待通りの処理になるようカッチリ制御しちゃいましょう。

addEventListenerの第三引数「useCapture」

例えば下記の様なHTMLがあったとします。
body内にdivが入れ子になっておりそれぞれにClickイベントが登録されています。

キャプチャとバブリング
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>event</title>
    <style>
        #outer {
            width: 150px; height: 150px;
            background: #00AAFF;
            padding-top: 50px; padding-left: 50px;
        }
        #inner {
            width:100px; height: 100px;
            background: #EEFF00
        }
    </style>
</head>
<body>
    <div id="outer">
        <div id="inner" align="center"></div>
    </div>
    <script>
        function out(s) {return function() {console.log(s);}}
        document.getElementById('outer').addEventListener('click', out('outer'), true);
        document.getElementById('inner').addEventListener('click', out('inner'));
    </script>
</body>
</html>

外側のdiv要素のaddEventListenerには第三引数にtrueをセットしています。
第三引数は内部的にはuseCaptureという名のフラグです。
このuseCaptureは省略可能でデフォルトはfalseとなっています。

内側要素クリック時の挙動

  • outerのuseCaptureをtrueにした場合

←outerが先

  • outerのuseCaptureをfalseまたは省略した場合

←innerが先

そう、useCaptureフラグの値によってイベントが発生する順序が逆になるのです!

イベントフェーズとは?

イベントが発生すると下記の流れでイベント伝播が発生します。

  • (キャプチャフェーズ) DOMツリーをたどってルート要素から発生要素を探しに行く
  • (ターゲットフェーズ) 発生要素を検出する
  • (バブリングフェーズ) 今度はルート要素まで遡る

W3Cのドキュメントに載っていた図がわかりやすいので拝借します。

親要素が同種のイベント(今回はClickイベント)を持っていた場合、
キャプチャフェーズでイベントを発生させてしまうよう設定するのがuseCapture=trueという事です。
逆にuseCaptureをfalseまたは省略すると親要素のイベントはバブリングフェーズで実行されます。

内側をクリックしたときに外側のイベントを発生させない

さて、もうひとつまとめておきます。
そもそも、イベント伝播実行自体やめてくれー
というケースは結構あると思います。
内側要素クリック時は内側要素のイベントだけ動けばええんや。。
というケースでは、最初のHTML例のscript部分を下記のようにします。

<script>
  function out(s) {
    return function(e) {
      e.stopPropagation();
      console.log(s);
    }
  }
  document.getElementById('outer').addEventListener('click', out('outer'));
  document.getElementById('inner').addEventListener('click', out('inner'));
</script>

変更ポイントは下記です。

  • イベント関数に引数eを渡し、e.stopPropagation();を実行している
  • 親要素のaddEventListenerのuseCaptureは省略(バブリングフェーズ実行)している

イベントオブジェクト

イベントで実行される関数は暗黙的に引数にイベントオブジェクトを受け取ります。

event.stopPropagationメソッド

イベントオブジェクトが持つstopPropagation()メソッドを実行するとイベント伝播が停止されます。
つまりバブリングフェーズで外側要素のイベントが発生しなくなるという事ですね。

ちなみに外側要素のuseCaptureをtrueにしてしまうと
キャプチャフェーズで外側要素のイベントが実行され、そこでイベント伝播を止めてしまいます。
ターゲットフェーズで本来クリックされた要素のイベントが発生されなくなる事にご注意下さい。


アチラコチラにイベントをセットしていったら伝播しまくりでパニック!なんて事にはならないよう
イベントフェーズはしっかり意識しておきたいなと思います。

今回は以上です。