addEventListener再入門 第1回 バブリングによるイベントの伝播
addEventListenerの第三引数useCaptureを利用して問題を解決した例から、イベントの仕組みを解説していきます。
- カテゴリー
- JavaScript >
- DOM
発行
はじめに
JavaScriptを書いていく上で必要な知識というのは、たくさんあります。それは例えば、JavaScriptの文法のルールであったり、ブラウザの挙動であったり、オブジェクト指向的な考え方であったり、jQueryやBackbone.jsなどの具体的なライブラリの使い方であったりします。
このシリーズでは、基礎と言えば基礎に分類されるけれど、普段何気なく書いているがゆえに、そこまで意識しなかった仕様や、仕組みを改めて見直すことでJavaScriptに対する理解をより深められるようなトピックを選び、解説していきます。
取り上げるテーマは、addEventListenerについてです。addEventListenerの第三引数useCaptureを利用し、問題を解決した例を紹介しながらイベントの仕組みを解説していきます。
なお、今回紹介するコードのサンプルは次のaddEventListenerサンプルリポジトリにまとめてあります。併せて参照してください。
addEventListenerサンプルリポジトリ
addEventListenerとは
まず、addEventListenerとはなんでしょう。これは基礎的な内容であるため、ここではサラッと解説するにとどめますが、addEventListenerとは、DOMの仕様で用意されている、イベントリスナーを登録するためのメソッドです。クリック、マウスオーバーなど、ブラウザ上ではさまざまなイベントが発生します。addEventListenerを使用すれば、これらのイベントが発生した時、指定したfunctionを実行することができます。このように、イベントの発生に合わせて実行させるfunctionのことを、イベントリスナーと呼びます。
addEventListenerを利用した、ごく単純なデモを見てみましょう。
HTML
<div id="foo">click me!</div>
JavaScript
var el = document.getElementById('foo');
el.addEventListener('click', function(e) {
e.preventDefault();
alert('foo clicked!');
alert(e.pageX); // 120 とか
});
上記は、id属性がfoo
である要素(以降、#foo
のように記述します)に、クリックイベントを設定しています。addEventListenerを利用してイベント名とイベントリスナーを指定すると、指定したイベントが発生した時、イベントリスナーの内容が実行されます。イベントリスナー内では、引数に渡されたイベントオブジェクトを介し、発生したイベントに関する種々の情報を得ることができます。
このデモでは、#foo
がクリックされたら、まずは「foo clicked!」とアラートされ、その次に、クリックされた場所のページ上でのX座標が表示されます。
このようにイベントを設定できるaddEventListenerですが、大昔から問題なく利用可能だったわけではありません。このメソッドがInternet Explorerに実装されたのはバージョン9からであり、それ以前のバージョンのIEでは、attachEventという別のメソッドを利用し、ブラウザ上で起こるイベントを設定する必要がありました。そういった面倒な状況があったがゆえに、簡単にイベントを扱えるようにしてくれるjQueryなどのライブラリが便利に利用されてきました。
しかし、比較的新しい環境や、スマートフォンのブラウザを対象とする場合は、そのような旧IEを考慮する必要がありません。問題なくaddEventListenerを利用することができます。
また、addEventListenerをそのまま使う場合、第三引数のuseCaptureを指定することで、より細かなイベント制御を行うことができるというのも、大きな利点です。
el.addEventListener('click', fn, true);
このuseCaptureというものは、jQueryの.on()
では対応していません。ですので、この機能が必要であれば、どのみちaddEventListenerをそのまま使う必要があります。いざというときにパッと使えるよう、addEventListenerとイベントの仕組みをおさらいしてみましょう。
今回のお題
addEventListenerの第三引数が便利なものであると述べましたが、正直なところ、これを使うケースはそれほど多くはないです。しかし、これを使わなければ解決できないケースというのは存在します。その場合、イベントの仕組みを正確に把握していないと四苦八苦するはずです。まずは、「素直に実装したら困ってしまった例」を見てみましょう。これを、今回解決するお題とします。
次のデモは、デスクトップ環境で動作します。このデモには2つの機能が同居しています。まずひとつは、カウンタ増加の機能。赤いdiv(#counter
)をクリックすると、中に書かれた数字が+1されます。そしてもうひとつが、ドラッグ移動の機能。ピンクのdiv(#floater
)は、ドラッグで好きな場所に移動することができます。
ざっとコードを見てみましょう。#floater
のドラッグ部分は、mousedown
、mousemove
、mouseup
で実装し、#counter
のカウント増加の部分は、単純なclick
で実装しています。
HTML
<div id="floater">
<div id="counter">0</div>
</div>
JavaScript
// 要素準備
var floater = document.getElementById('floater');
var counter = document.getElementById('counter');
// ドラッグ中かを記憶するフラグ
var whileDrag = false;
// クリックされたらカウント増加
counter.addEventListener('click', function() {
var current = parseInt(counter.textContent, 10); // 中の数字
counter.textContent = current + 1; // +1 して突っ込む
});
// mousedownでドラッグ開始
floater.addEventListener('mousedown', function() {
whileDrag = true;
});
// mousemoveでマウス位置にfloaterを追従させる
document.addEventListener('mousemove', function(e) {
if(!whileDrag) { return; }
var x = e.pageX;
var y = e.pageY;
// floaterは絶対配置。left, topを更新してつまんだ位置へ移動。
floater.style.left = (x - 30) + 'px';
floater.style.top = (y - 30) + 'px';
});
// mouseupでドラッグ完了
floater.addEventListener('mouseup', function() {
whileDrag = false;
});
このお題での問題点
一見問題なさそうなこのデモですが、よくよく操作してみると、困ったことがひとつあることに気付きます。それは、ただドラッグ移動しただけでも、カウンタ増加が必ず発生してしまうということです。ピンクの部分をつまんでドラッグすればこのようなことは起こりませんが、赤い部分をドラッグすると発生します。
#counter
上でマウスのボタンを押し、そのまま動かし、ボタンを離すと、#counter
がドラッグされたということになりますが、#floater
は#counter
を内包しているため、同時に#floater
がドラッグされたことにもなります。つまり、#counter
上でドラッグすれば、#coutner
でも#floater
でも「ドラッグされた」ことになります。
ここで「ドラッグ」と言っているのは、mousedown
してmousemove
し、最後にmouseup
される一連の動作のことです。では、同一の要素上でmousedown
し、mouseup
したらどうなるでしょう? これは「クリック」と呼ばれる動作となります。#counter
について考えてみると、要素上でmousedown
が発生し、次にmousemove
が発生しているものの、その後にまたmouseup
が発生しています。ブラウザは、これを#counter
でクリックイベントが発生したと捉えるようです。
つまり、#counter
上でドラッグすれば、#counter
は必ずクリックされたことにもなるというわけです。2つの機能をそれぞれ作っているだけなのですが、ドラッグ時にカウント増加をさせないためには、何かしらの工夫が必要になります。
コラム:クリックとは
当初、「クリックの定義って何だ? ドラッグしたらマウスの位置がずれてるんだから、クリックにならないんじゃないの?」と筆者は思いました。しかし、DOMのspecを見たところ、p
要素上でマウスをドラッグしてテキストを選択するような動作が行われた場合、mousedown
もmouseup
も、その要素上で起こったのであれば、ブラウザはクリックイベントを発生させるという旨の例が書かれていました。本稿の例も、ドラッグして要素が移動しているものの、mousedown
もmouseup
も同一の#counter
上で発生しているので、クリックが発生しているということになるようです。
イベントの伝播のしかたを理解する
前途した問題は、発生したイベントがどのように伝わっていくかという仕組みを細かく把握していれば、解決することができます。そのためにはまず、「バブリング」について理解する必要があります。至極単純なデモで、イベントの基本的な動作を確認してみましょう。
HTML
<div id="div1">
#div1
<div id="div2">
#div2
<div id="div3">#div3</div>
</div>
</div>
JavaScript
var div1 = document.getElementById('div1');
var div2 = document.getElementById('div2');
var div3 = document.getElementById('div3');
div1.addEventListener('click', function() {
alert('Hello! I am #div1.');
});
div2.addEventListener('click', function() {
alert('Hello! I am #div2.');
});
div3.addEventListener('click', function() {
alert('Hello! I am #div3.');
});
ここでは、#div1 > #div2 > #div3
と、三重の入れ子になったdivにそれぞれクリックイベントを設定し、自身のidをアラートするようにしています。
#div1
をクリックすれば「Hello! I am #div1.」とアラートされます。しかし、#div2
と#div3
をクリックした場合は、自身に設定されたアラートが出るだけではありません。
#div2
をクリックすれば
- Hello! I am #div2.
- Hello! I am #div1.
と2回、アラートされます。
#div3
をクリックすれば
- Hello! I am #div3.
- Hello! I am #div2.
- Hello! I am #div1.
と3回、アラートされます。
何かしらのイベントが発生し、そのイベントが複数の要素に関わる場合、ブラウザは、最も深い階層にある要素から順にイベントを評価します。ここで最も深い階層にあるのは#div3
です。このため、#div3
に設定したクリックのイベントリスナーが実行されました。そして次にこれを囲む#div2
、その次は#div2
を囲む#div1
と、順々にイベントリスナーが実行されたのです。
このデモのように、下位階層の要素から順々に上位階層のイベントが評価されることを、バブリングと言います。泡のようにポコポコと、下から上に上がって実行されるので、そのような名前なのではないかと思われます。この概念については、過去にCodeGridでも取り上げたことがあるので参考にしてみてください。
このように入れ子になった要素において、イベントが順々に評価されることをイベントの伝播と言うことがあります。#div3
がクリックされたあとに#div2
、#div1
と、イベントが伝わっていくような動作になるためです。
イベントの伝播を止める
しかし、イベントの伝播をさせたくない場合もあります。今回の例では、#div3
がクリックされたのであれば、#div2
や#div1
がクリックされたことにはしないでほしいということです。
そのためにはイベントの伝播を止める必要があるので、イベントオブジェクトのstopPropagation
というメソッドを実行します。デモを見てみましょう。このデモのHTMLは、先ほどのデモと全く同じで、異なるのはJavaScript内の、#div3
に設定したイベントリスナーのみです。
JavaScript
var div1 = document.getElementById('div1');
var div2 = document.getElementById('div2');
var div3 = document.getElementById('div3');
div1.addEventListener('click', function() {
alert('Hello! I am #div1.');
});
div2.addEventListener('click', function() {
alert('Hello! I am #div2.');
});
div3.addEventListener('click', function(e) {
alert('Hello! I am #div3.');
e.stopPropagation(); // イベントの伝播を止める
});
このデモにおいて、#div1
、#div2
をクリックした時の挙動はひとつ前のデモと同様ですが、#div3
をクリックした場合はちょっと違います。「Hello! I am #div3.」とだけ表示されます。これは、#div3
のクリックイベントリスナー内で、e.stopPropagation()
が実行されているためです。e.stopPropagation()
が実行された時点でイベントの伝播は止まるため、#div2
、#div1
に設定されたイベントリスナーは実行されなくなります。これがイベントの伝播を止める方法です。
ここまでのまとめ
次回以降、「ボタンをクリックするとカウンタ増加するが、ドラッグ移動した場合にはカウンタ増加をさせたくない」というお題を、addEventListenerの第三引数を利用して解決していきます。具体的な解決方法の前に、まず今回は発生したイベントがどのように伝わっていくかという仕組みを把握するために、「バブリング」をおさらいしました。
次回は、addEventListenerの第三引数useCaptureの使い方を見ていきます。stopPropagationとuseCaptureを併用することで、イベントを柔軟に制御することができる様子を解説しましょう。今回のお題の問題解決の手がかりになるはずです。