【JavaScript】イベントの伝搬 - キャプチャリング・バブリング

【JavaScript】イベントの伝搬 - キャプチャリング・バブリング

JavaScript/ブラウザイベントにおける『イベントの伝搬』について解説します。

検証環境

イベントの伝搬

イベントの伝搬は“イベントが要素(オブジェクト)に伝わっていくこと”です。

伝搬には次の3つのフェーズがあります。

  1. キャプチャリングフェーズ
  2. ターゲットフェーズ
  3. バブリングフェーズ

各フェーズについて、以下に解説します。

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

キャプチャリングは“最上位オブジェクトからイベント発生源の要素まで伝搬すること”です。

具体的には次の順序でキャプチャリングが行われます。

  1. Windowオブジェクト(最上位オブジェクト)
  2. Documentオブジェクト
  3. html
  4. 以降、要素の親子関係に沿ってイベント発生源前までの伝搬

伝搬時のイベントハンドラーはaddEventListenerメソッドの第3引数にtrueまたは{ capture: true }を与えることで登録できます。

次のサンプルをご覧ください。

<!DOCTYPE html>
<html id="html">
    <head id="head">
        <title>サンプル</title>
        <meta charset="UTF-8">
        <style>
            #log {
                border: 1px solid black;
                padding: 5px;
            }
        </style>
    </head>
    <body id="body">
        <div id="main">
            <button id="btn">確認</button>
        </div>
        <pre id="log"></pre>
        <input type="button" id="reset" value="リセット">
        
        <script type="text/javascript">
            // 要素のイベントハンドラー
            function logger(event) {
                let phase = phase_name(event.eventPhase);
                let name = current_target_name(event.currentTarget);
                
                let log = document.getElementById('log');
                log.textContent += "[ " + phase + " ] " + name + "\n";
            }
            
            // フェーズ
            function phase_name(phase_code) {
                if (phase_code == 1) {
                    return "CAPTURING";
                } else if (phase_code == 2) {
                    return "TARGET";
                } else if (phase_code == 3) {
                    return "BUBBLING";
                }
                return "";
            }
            
            // オブジェクト・要素名
            function current_target_name(currentTarget) {
                if (currentTarget instanceof Window) {
                    return "Window";
                } else if (currentTarget instanceof Document) {
                    return "Document";
                }
                return currentTarget.tagName;
            }
            
            ___ih_hl_start
            // オブジェクト・要素
            let targets = [
                // div要素
                document.getElementById('main'),
            ];
            ___ih_hl_end
            
            ___ih_hl_start
            // イベントハンドラーの登録
            for( target of targets ) {
                target.addEventListener('click', logger, { capture: true });
            }
            ___ih_hl_end
            
            // ログのリセット
            let reset = document.getElementById('reset');
            reset.addEventListener('click', function (event) {
                let log = document.getElementById('log');
                log.textContent = "";
                event.stopPropagation();
            }, { capture: true });
        </script>
    </body>
</html>

このコードは以降の解説でコードを追加しながら使うため、拡張性あるコードにしています。

52〜56行目の配列targetsはイベントを登録するオブジェクトや要素の一覧です。

ここではdiv要素のみを記憶しています。

58〜61行目でtargetsの各オブジェクト・要素について、キャプチャリング時のイベントハンドラーとしてlogger関数を登録しています。

logger関数はイベント内容をレンダリング(表示)する処理を持っており、フェーズをphase_name関数、イベントハンドラー登録対象名をcurrent_target_name関数で取得します。

また、23行目のEventオブジェクトのeventPhaseプロパティはフェーズを表し、値が1はキャプチャリングフェーズ、2はターゲットフェーズ、3はバブリングフェーズに対応します。

プレビューの確認ボタンをクリックすると、表示される内容からキャプチャリングフェーズかつbutton要素の親であるdiv要素のイベントが実行されたことが分かります。


更にキャプチャリングの順序を確認できるようWindowオブジェクト、Documentオブジェクト、html要素、body要素も追加します。

<!DOCTYPE html>
<html id="html">
    <head id="head">
        <title>サンプル</title>
        <meta charset="UTF-8">
        <style>
            #log {
                border: 1px solid black;
                padding: 5px;
            }
        </style>
    </head>
    <body id="body">
        <div id="main">
            <button id="btn">確認</button>
        </div>
        <pre id="log"></pre>
        <input type="button" id="reset" value="リセット">
        
        <script type="text/javascript">
            // 要素のイベントハンドラー
            function logger(event) {
                let phase = phase_name(event.eventPhase);
                let name = current_target_name(event.currentTarget);
                
                let log = document.getElementById('log');
                log.textContent += "[ " + phase + " ] " + name + "\n";
            }
            
            // フェーズ
            function phase_name(phase_code) {
                if (phase_code == 1) {
                    return "CAPTURING";
                } else if (phase_code == 2) {
                    return "TARGET";
                } else if (phase_code == 3) {
                    return "BUBBLING";
                }
                return "";
            }
            
            // オブジェクト・要素名
            function current_target_name(currentTarget) {
                if (currentTarget instanceof Window) {
                    return "Window";
                } else if (currentTarget instanceof Document) {
                    return "Document";
                }
                return currentTarget.tagName;
            }
            
            // オブジェクト・要素
            let targets = [
                ___ih_diff_start
+                // Windowオブジェクト
+                window,
+                // Documentオブジェクト
+                document,
+                // html要素
+                document.getElementById('html'),
+                // body要素
+                document.getElementById('body'),
                ___ih_diff_end
                // div要素
                document.getElementById('main'),
            ];
            
            // イベントハンドラーの登録
            for( target of targets ) {
                target.addEventListener('click', logger, { capture: true });
            }
            
            // ログのリセット
            let reset = document.getElementById('reset');
            reset.addEventListener('click', function (event) {
                let log = document.getElementById('log');
                log.textContent = "";
                event.stopPropagation();
            }, { capture: true });
        </script>
    </body>
</html>

配列targetsに各オブジェクト・要素を追加しました。(54〜61行目)

プレビューの確認ボタンをクリックし、表示される順番からキャプチャリングが最上位から順番に伝搬していることが確認できます。

ターゲットフェーズ

ターゲットフェーズは伝搬が“イベント発生源の要素(ターゲット)に到達した段階のこと”です。

キャプチャリングの最後であり、バブリングの最初の段階でもあります。

例えば、上記サンプルのボタンをクリックした場合、button要素に到達した段階です。

キャプチャリングの順序を確認できるようbutton要素も追加します。

<!DOCTYPE html>
<html id="html">
    <head id="head">
        <title>サンプル</title>
        <meta charset="UTF-8">
        <style>
            #log {
                border: 1px solid black;
                padding: 5px;
            }
        </style>
    </head>
    <body id="body">
        <div id="main">
            <button id="btn">確認</button>
        </div>
        <pre id="log"></pre>
        <input type="button" id="reset" value="リセット">
        
        <script type="text/javascript">
            // 要素のイベントハンドラー
            function logger(event) {
                let phase = phase_name(event.eventPhase);
                let name = current_target_name(event.currentTarget);
                
                let log = document.getElementById('log');
                log.textContent += "[ " + phase + " ] " + name + "\n";
            }
            
            // フェーズ
            function phase_name(phase_code) {
                if (phase_code == 1) {
                    return "CAPTURING";
                } else if (phase_code == 2) {
                    return "TARGET";
                } else if (phase_code == 3) {
                    return "BUBBLING";
                }
                return "";
            }
            
            // オブジェクト・要素名
            function current_target_name(currentTarget) {
                if (currentTarget instanceof Window) {
                    return "Window";
                } else if (currentTarget instanceof Document) {
                    return "Document";
                }
                return currentTarget.tagName;
            }
            
            // オブジェクト・要素
            let targets = [
                // Windowオブジェクト
                window,
                // Documentオブジェクト
                document,
                // html要素
                document.getElementById('html'),
                // body要素
                document.getElementById('body'),
                // div要素
                document.getElementById('main'),
                ___ih_diff_start
+                // button要素
+                document.getElementById('btn'),
                ___ih_diff_end
            ];
            
            // イベントハンドラーの登録
            for( target of targets ) {
                target.addEventListener('click', logger, { capture: true });
            }
            
            // ログのリセット
            let reset = document.getElementById('reset');
            reset.addEventListener('click', function (event) {
                let log = document.getElementById('log');
                log.textContent = "";
                event.stopPropagation();
            }, { capture: true });
        </script>
    </body>
</html>

配列targetsbutton要素を追加しました。(64〜65行目)

プレビューの確認ボタンをクリックし、表示される内容からbutton要素(BUTTON)に到達した段階がターゲットフェーズであることが分かります。

バブリングフェーズ

バブリングは“イベント発生源の要素からWindowオブジェクトまで伝搬すること”です。

順序はキャプチャリングと逆のイメージで、具体的には次の順序でバブリングが行われます。

  1. イベント発生源の親要素
  2. 以降、親要素を1つ1つ辿って伝搬
  3. html
  4. Document
  5. Window

また、一般的に最もイベントハンドラーを実行するタイミングとして利用し、addEventListenerメソッドを使用してイベントハンドラーを登録します。

キャプチャリングの順序を確認できるようバブリングフェーズのイベントハンドラーも登録します。

<!DOCTYPE html>
<html id="html">
    <head id="head">
        <title>サンプル</title>
        <meta charset="UTF-8">
        <style>
            #log {
                border: 1px solid black;
                padding: 5px;
            }
        </style>
    </head>
    <body id="body">
        <div id="main">
            <button id="btn">確認</button>
        </div>
        <pre id="log"></pre>
        <input type="button" id="reset" value="リセット">
        
        <script type="text/javascript">
            // 要素のイベントハンドラー
            function logger(event) {
                let phase = phase_name(event.eventPhase);
                let name = current_target_name(event.currentTarget);
                
                let log = document.getElementById('log');
                log.textContent += "[ " + phase + " ] " + name + "\n";
            }
            
            // フェーズ
            function phase_name(phase_code) {
                if (phase_code == 1) {
                    return "CAPTURING";
                } else if (phase_code == 2) {
                    return "TARGET";
                } else if (phase_code == 3) {
                    return "BUBBLING";
                }
                return "";
            }
            
            // オブジェクト・要素名
            function current_target_name(currentTarget) {
                if (currentTarget instanceof Window) {
                    return "Window";
                } else if (currentTarget instanceof Document) {
                    return "Document";
                }
                return currentTarget.tagName;
            }
            
            // オブジェクト・要素
            let targets = [
                // Windowオブジェクト
                window,
                // Documentオブジェクト
                document,
                // html要素
                document.getElementById('html'),
                // body要素
                document.getElementById('body'),
                // div要素
                document.getElementById('main'),
                // button要素
                document.getElementById('btn'),
            ];
            
            // イベントハンドラーの登録
            for( target of targets ) {
                target.addEventListener('click', logger, { capture: true });
                ___ih_diff_start
+                target.addEventListener('click', logger);
                ___ih_diff_end
            }
            
            // ログのリセット
            let reset = document.getElementById('reset');
            reset.addEventListener('click', function (event) {
                let log = document.getElementById('log');
                log.textContent = "";
                event.stopPropagation();
            }, { capture: true });
        </script>
    </body>
</html>

71行目がバブリングフェーズのイベントハンドラーの登録です。

プレビューの確認ボタンをクリックし、表示される内容の[ BUBBLING ]の部分からターゲット要素から親へ1つずつ伝搬したことが分かります。

また、[ TARGET ]が2つ表示されたのは、ターゲット要素のキャプチャリング時とバブリング時のイベントハンドラーがいずれも実行されたためです。