【JavaScript】イベントの伝搬 - キャプチャリング・バブリング
JavaScript/ブラウザイベントにおける『イベントの伝搬』について解説します。
検証環境
イベントの伝搬
イベントの伝搬は“イベントが要素(オブジェクト)に伝わっていくこと”です。
伝搬には次の3つのフェーズがあります。
- キャプチャリングフェーズ
- ターゲットフェーズ
- バブリングフェーズ
各フェーズについて、以下に解説します。
キャプチャリングフェーズ
キャプチャリングは“最上位オブジェクトからイベント発生源の要素まで伝搬すること”です。
具体的には次の順序でキャプチャリングが行われます。
- Windowオブジェクト(最上位オブジェクト)
- Documentオブジェクト
- html
- 以降、要素の親子関係に沿ってイベント発生源前までの伝搬
伝搬時のイベントハンドラーは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>
配列targets
にbutton
要素を追加しました。(64〜65行目)
プレビューの確認ボタンをクリックし、表示される内容からbutton
要素(BUTTON
)に到達した段階がターゲットフェーズであることが分かります。
バブリングフェーズ
バブリングは“イベント発生源の要素からWindowオブジェクトまで伝搬すること”です。
順序はキャプチャリングと逆のイメージで、具体的には次の順序でバブリングが行われます。
- イベント発生源の親要素
- 以降、親要素を1つ1つ辿って伝搬
- html
- Document
- 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つ表示されたのは、ターゲット要素のキャプチャリング時とバブリング時のイベントハンドラーがいずれも実行されたためです。