サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

EaselJSのイベントリスナーにFunction.bind()メソッドを適用するとリスナー内から削除できない

ID: FN1302002 Technique: HTML5 and JavaScript

2013年1月づけでCreateJSサイトにつぎのリリース候補バージョン(release candidates)が公開されました。新しいEaselJSライブラリには、イベントリスナーの仕組みが採入れられています。ひとつ注意したいのは、イベントのリスナーとして加える関数にFunction.bind()メソッドを適用すると、リスナー関数の中からそのリスナー自身が単純には除けないことです。


01 登録するリスナー関数になぜFunction.bind()メソッドを適用するのか

まず、登録するリスナー関数に、なぜFunction.bind()メソッドを適用するのかからご説明します。具体的な例としては、マウスボタンを放したとき、そのリスナー関数の中からそのリスナー関数自身を除く場合が挙げられます。たとえば、インスタンスがクリック(マウスボタンの押下げ)されたDisplayObject.mousedownイベントで、MouseEvent.mouseupイベントにリスナーを加えたとします。

そして、マウスボタンを放したとき、リスナー関数の中からEventDispatcher.removeEventListener()メソッドでそのリスナー自身を除きたいと思います。そのとき、リスナーが登録されたMouseEventオブジェクトを参照しなければなりません。リスナー関数が受取ったMouseEventオブジェクトのMouseEvent.typeプロパティは"mouseup"であって、リスナーを登録したオブジェクト(MouseEvent.typeプロパティは"mousedown")とは別ものです。マウス操作しているインスタンスはMouseEvent.targetプロパティでとれても、リスナーを加えたオブジェクトはMouseEventオブジェクトのプロパティにありません。

instance.addEventListener("mousedown", press);
function press(eventObject) {
  eventObject.addEventListener("mouseup", release);
}
function release(eventObject) {
  // マウスボタンを放したときの処理
  参照.removeEventListener("mouseup", release);
}

リリース候補バージョンのドキュメントを見ると、リスナー関数の中のthis参照を変える手としてFunction.bind()メソッドが紹介されています(図001)。メソッドの引数に渡したオブジェクトが、呼出した関数の中でthis参照されるのです。

図001■リスナー関数内のスコープを変える方法としてFunction.bind()メソッドが挙げられている
図001

テスト用のサンプルコードを書いてみましょう。script要素全体は、後にコード001として掲げます。CanvasにShapeインスタンスを置いて、そのDisplayObject.mousedownイベントでMouseEvent.mouseupイベントにリスナーを加え(図002)、そのリスナー関数の中からリスナー自身を除きます。

図002■インスタンス上でマウスボタンを押すとMouseEvent.mouseupイベントにリスナーが加えられる
図002

以下に後掲コード001からマウスイベントに関わるスクリプトを抜書きしました。まず、Shapeインスタンス(myShape)のDisplayObject.mousedownイベントにリスナー(press())が加えられています(第18行目)。

つぎに、インスタンス上でマウスボタンを押すと、リスナー関数(press())がMouseEvent.mouseupイベントにリスナー関数を登録します(第21行目)。このとき渡す関数にFunction.bind()メソッドを適用して、イベントリスナーが登録されたMouseEventオブジェクトを引数に渡しています。

そして、マウスボタンを放すと、リスナー関数(release())の中からthisを参照してEventDispatcher.removeEventListener()メソッドが呼出され、リスナー関数自身を除こうとします(第27行目)。ところが、イベントリスナーは削除されません。

  1.   myShape.addEventListener("mousedown", press);
  1. function press(eventObject) {
  2.   eventObject.addEventListener("mouseup", release.bind(eventObject));
  3. }
  4. function release(eventObject) {
  5.   var result = [];
  6.   result.push(this.hasEventListener("mouseup"));
  7.   this.removeEventListener("mouseup", release);
  8.   result.push(this.hasEventListener("mouseup"));
  9.   alert(result);   // 表示: true, true
  10. }

オブジェクトのイベントにリスナーが加えられているかどうかは、EventDispatcher.hasEventListener()メソッドで調べられます。引数にイベントを文字列で渡すと、リスナーがあればtrue、なければfalseのブール(論理)値で返されます。

オブジェクト.hasEventListener(イベント)

MouseEvent.mouseupイベントのリスナー関数(release())は、EventDispatcher.removeEventListener()メソッドが呼出される前と後に、それぞれEventDispatcher.hasEventListener()メソッドの戻り値を配列(result)に入れています(第25および第27行目)。そして、その配列エレメントを、Window.alert()メソッドで警告ダイアログボックスに表示します(第28行目)。エレメント値はふたつともtrueなので、リスナーが除かれていないことを示します(図003)。

図003■警告ダイアログボックスはEventDispatcher.removeEventListener()メソッドでリスナーが除かれてないことを示す
図003

Function.bind()メソッドが適用されたリスナー関数の削除を試したscript要素全体は、つぎのコード001のとおりです。

コード001■Function.bind()メソッドが適用されたリスナー関数からリスナー自身を除く
  1. <script src="easeljs/utils/UID.js"></script>
  2. <script src="easeljs/geom/Matrix2D.js"></script>
  3. <script src="easeljs/events/EventDispatcher.js"></script>
  4. <script src="easeljs/events/MouseEvent.js"></script>
  5. <script src="easeljs/display/DisplayObject.js"></script>
  6. <script src="easeljs/display/Container.js"></script>
  7. <script src="easeljs/display/Stage.js"></script>
  8. <script src="easeljs/display/Graphics.js"></script>
  9. <script src="easeljs/display/Shape.js"></script>
  10. <script>
  11. var stage;
  12. function initialize() {
  13.   var canvasElement = document.getElementById("myCanvas");
  14.   stage = new createjs.Stage(canvasElement);
  15.   var myShape = new createjs.Shape();
  16.   stage.addChild(myShape);
  17.   draw(myShape.graphics);
  18.   myShape.addEventListener("mousedown", press);
  19. }
  20. function press(eventObject) {
  21.   eventObject.addEventListener("mouseup", release.bind(eventObject));
  22. }
  23. function release(eventObject) {
  24.   var result = [];
  25.   result.push(this.hasEventListener("mouseup"));
  26.   this.removeEventListener("mouseup", release);
  27.   result.push(this.hasEventListener("mouseup"));
  28.   alert(result);   // 表示: true, true
  29. }
  30. function draw(myGraphics) {
  31.   myGraphics.beginFill("#0000FF");
  32.   myGraphics.drawRect(0, 0, 50, 50);
  33.   stage.update();
  34. }
  35. </script>

02 削除するリスナー関数にはFunction.bind()メソッドを使わない

前掲コード001でリスナー関数が除けなかったのは、Function.bind()メソッドが新たな関数を返すからです。Function.bind()メソッドは、参照した関数にもとづき、引数のオブジェクトがthis参照となる新たな関数を返すのです。したがって、参照した関数と(this参照を除いて)同じ内容であっても、戻り値の関数は別ものなのです。たとえば、つぎのようにふたつの関数を等価比較すれば、警告ダイアログボックスにfalseが示されます。

var listener = release.bind(eventObject);
alert(listener === release);   // 表示: false
function release() {
  // ...[中略]...
}

ですから、リスナー関数をイベントから除きたい場合には、その関数にFunction.bind()メソッドは適用しない方がよいでしょう。すると、初めの問に戻って、リスナーが加えられたオブジェクトの参照の仕方を考えなければなりません。ひとつは、グローバルな変数にオブジェクトをとっておく手があります。けれどもうひとつ、リスナー関数からMouseEvent.targetプロパティで得られるオブジェクトに変数を定める方が応用の幅は広がりそうです。

すると、JavaScriptコードは、つぎのように書替えればよいでしょう。行番号は後に全体を掲げるコード002にもとづきます。DisplayObject.mousedownイベントのリスナー関数(press())は、引数のMouseEvent.targetプロパティから得たマウス操作の対象オブジェクト(instance)に変数(dispatcher)を定め、MouseEventオブジェクトを与えます(第11〜12行目)。

すると、そのMouseEventオブジェクトのMouseEvent.mouseupイベントに加えたリスナー関数(release())も、引数のMouseEvent.targetプロパティから同じマウス操作の対象オブジェクトが得られます。そうすれば、オブジェクトの変数でリスナーを登録したMouseEventオブジェクト(dispatcher)が取出せます(第16行目)。あとは、そのMouseEventオブジェクトから、EventDispatcher.removeEventListener()メソッドでリスナーを削除するだけです(第19行目)。

  1. function press(eventObject) {
  2.   var instance = eventObject.target;
  3.   instance.dispatcher = eventObject;
  4.   eventObject.addEventListener("mouseup", release);   // .bind(eventObject));
  5. }
  6. function release(eventObject) {
  7.   var dispatcher = eventObject.target.dispatcher;
      // this.removeEventListener("mouseup", release);
  1.   dispatcher.removeEventListener("mouseup", release);
  1. }

書替えたJavaScriptコード002は、前掲コード001と同じく、EventDispatcher.removeEventListener()メソッドの呼出し前と後に、EventDispatcher.hasEventListener()メソッドでリスナーのあるなしを確かめています((第18〜20行目))。今度は削除後にメソッドの戻り値がfalseになりましたので、リスナーは確かに除かれています(図004)。

コード002■MouseEvent.targetプロパティの変数にリスナーが登録されたオブジェクトを定める
  1. var stage;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  4.   stage = new createjs.Stage(canvasElement);
  5.   var myShape = new createjs.Shape();
  6.   stage.addChild(myShape);
  7.   draw(myShape.graphics);
  8.   myShape.addEventListener("mousedown", press);
  9. }
  10. function press(eventObject) {
  11.   var instance = eventObject.target;
  12.   instance.dispatcher = eventObject;
  13.   eventObject.addEventListener("mouseup", release);
  14. }
  15. function release(eventObject) {
  16.   var dispatcher = eventObject.target.dispatcher;
  17.   var result = [];
  18.   result.push(dispatcher.hasEventListener("mouseup"));
  19.   dispatcher.removeEventListener("mouseup", release);
  20.   result.push(dispatcher.hasEventListener("mouseup"));
  21.   alert(result);
  22. }
  23. function draw(myGraphics) {
  24.   myGraphics.beginFill("#0000FF");
  25.   myGraphics.drawRect(0, 0, 50, 50);
  26.   stage.update();
  27. }

図004■警告ダイアログボックスはEventDispatcher.removeEventListener()メソッドでリスナーが除かれたことを示す
図004


03 Function.bind()メソッドが適用されたリスナー関数を削除するには

リスナー関数にFunction.bind()メソッドを適用したうえで、そのリスナー関数をイベントから除かなければならい場合について触れておきます。少なくとも、EventDispatcher.addEventListener()メソッドに渡す第2引数のリスナー関数に、直にFunction.bind()メソッドを適用すべきではありません。それでは、Function.bind()メソッドがつくった新たな関数の参照が残らないからです。

Function.bind()メソッドの戻り値となる関数は、必ず変数にとります。その参照をリスナー関数から得るには、前項の考え方が使えるでしょう。つまり、MouseEvent.targetプロパティが参照するオブジェクトに、変数で納めればよいのです。前掲コード001であれば、イベントDisplayObject.mousedownMouseEvent.mouseupのリスナー関数(press()とrelease())は、それぞれつぎのように書替えます。

  1. function press(eventObject) {
      var listener = release.bind(eventObject);
      eventObject.target.listener = listener;
      // eventObject.addEventListener("mouseup", release.bind(eventObject));
  1.   eventObject.addEventListener("mouseup", listener);
  2. }
  3. function release(eventObject) {
      var listener = eventObject.target.listener;
      // this.removeEventListener("mouseup", release);
  1.   this.removeEventListener("mouseup", listener);
  2. }

もっとも、このやり方であれば、MouseEvent.targetプロパティのオブジェクトにいくらでも変数で参照が加えられます。また細かい話しをすれば、新たにつくられた関数がメモリを費やすこともありません。ですから、あえてFunction.bind()メソッドでthis参照を変えなければならない場合は、かなりかぎられるでしょう。



作成者: 野中文雄
作成日: 2013年2月7日


Copyright © 2001-2013 Fumio Nonaka.  All rights reserved.