入門
第1回 バブリングによるイベントの伝播

addEventListenerの第三引数useCaptureを利用して問題を解決した例から、イベントの仕組みを解説していきます。

2015年05月28日発行

目次

JavaScriptを書いていく上で必要な知識というのは、たくさんあります。それは例えば、JavaScriptの文法のルールであったり、ブラウザの挙動であったり、オブジェクト指向的な考え方であったり、jQueryやBackbone.jsなどの具体的なライブラリの使い方であったりします。

このシリーズでは、基礎と言えば基礎に分類されるけれど、普段何気なく書いているがゆえに、そこまで意識しなかった仕様や、仕組みを改めて見直すことでJavaScriptに対する理解をより深められるようなトピックを選び、解説していきます。

取り上げるテーマは、addEventListenerについてです。addEventListenerの第三引数useCaptureを利用し、問題を解決した例を紹介しながらイベントの仕組みを解説していきます。

なお、今回紹介するコードのサンプルは次のaddEventListenerサンプルリポジトリにまとめてあります。併せて参照してください。

addEventListenerサンプルリポジトリ

addEventListenerとは

まず、addEventListenerとはなんでしょう。これは基礎的な内容であるため、ここではサラッと解説するにとどめますが、addEventListenerとは、DOMの仕様で用意されている、イベントリスナーを登録するためのメソッドです。クリック、マウスオーバーなど、ブラウザ上ではさまざまなイベントが発生します。addEventListenerを使用すれば、これらのイベントが発生した時、指定したfunctionを実行することができます。このように、イベントの発生に合わせて実行させるfunctionのことを、イベントリスナーと呼びます。

addEventListenerを利用した、ごく単純なデモを見てみましょう。

イベント名とイベントリスナーを指定した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のドラッグ部分は、mousedownmousemovemouseupで実装し、#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要素上でマウスをドラッグしてテキストを選択するような動作が行われた場合、mousedownmouseupも、その要素上で起こったのであれば、ブラウザはクリックイベントを発生させるという旨の例が書かれていました。本稿の例も、ドラッグして要素が移動しているものの、mousedownmouseupも同一の#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を併用することで、イベントを柔軟に制御することができる様子を解説しましょう。今回のお題の問題解決の手がかりになるはずです。

高津戸 壮
高津戸 壮
フロントエンド・エンジニア

Web制作会社、フリーランスを経て、株式会社ピクセルグリッドに入社。数多くのWebサイト、WebアプリケーションのHTML、CSS、JavaScript実装に携わってきた。受託案件を中心にフロント周りの実装、設計、テクニカルディレクションを行う。スケーラビリティを考慮したHTMLテンプレート設計・実装、JavaScriptを使った込み入ったUIの設計・実装を得意分野とする。 著書に『改訂版 Webデザイナーのための jQuery入門』(技術評論社、2014年11月14日)がある。 CSS Nite 2011ベストセッションにおいて、全170セッションの中から、ベスト10セッションに、CSS Nite 2013ベストセッションでは、全278セッション中、ベスト20セッションに選出。