Event.stopObservingが効かない・動作しない・削除できない。

presented by 役に立つかもしれないBlog

Prototype.jsはJavaScriptを使ってサイトを制作する開発者にとってもはや必携のライブラリとなっていますが、頻繁にバージョンアップが繰り返されたり、意図したとおりの挙動をしてくれなかったりと、イライラするところも多いライブラリです。

とくに、よくみなさんが引っかかるのがEvent.stopObservingの挙動。 マウスクリックによって呼び出されるハンドラ(イベント関数)をEvent.observeで登録して、それを後で無効にしたいときに使ったりしますが、実際やってみると削除できない! どうして? と思っていらっしゃった方も多いかと思います。

実際、この問題は世界的にメジャーなようで、Prototype.jsの公式APIでもFAQになっています。 英語なので翻訳してみます。 妙な意訳も入ってますが。。

stopObserving

Event.stopObserving(element, eventName, handler[, useCapture = false]);
(elementへEvent.observe()で登録した)イベント関数の登録を解除します。

この関数は、(handlerの部分に)Event.observe()と同じイベント関数を指定して呼び出してください。 そうすれば、そのイベント関数が登録解除されますので、イベントが発生してもこの要素(element)と関数のペアは2度と呼ばれなくなります。

ふむふむ。そうですね。

解除できなくなるのはどうして!?

Event.stopObservingを動作させるには、Event.observeしたときに指定したイベント関数と完全に一致する宣言(イベント関数の参照のことでしょう)を渡さなければいけません。 このルールを守るのは一見あたりまえのことのように感じるかもしれませんが、みなさんがよく書くコードのパターンの中で実はそうなっていないものがあるのです。

var obj = {
  ...
  fx: function(event) {
    ...
  }
};
Event.observe(elt, 'click', obj.fx.bindAsEventListener(obj));
...
// ↓これが間違い。 こう書いてはいけません。
Event.stopObserving(elt, 'click', obj.fx.bindAsEventListener(obj)); // 動きませんよ!

言われたとおりに書いているように見えますが...

このコードは一見素晴らしいように見えますが、bindAsEventListenerは呼ばれるたびにあなたが指定した関数をラップした新しい匿名関数を生成して返すことを覚えておかなくてはいけません。 つまり、新しい関数を返しているのです。 したがって、この例ではEvent.observe()で指定した関数とは異なる関数をstopObservingしていることになります。 当然、stopObservingで指定した関数がobservingされているわけがないので、Event.observeした関数が問題なく残ってしまうわけです。

はぁ...「問題なく」ね...

これを避けるためには、script.aculo.usが自身の多くのクラス内で書いているように関数をキャッシュするようにします。 例えば以下のような感じです。

var obj = {
  ...
  fx: function(event) {
    ...
  },
};
obj.bfx = obj.fx.bindAsEventListener(obj);
Event.observe(elt, 'click', obj.bfx);
...
Event.stopObserving(elt, 'click', obj.bfx);

(翻訳終了)

「イベント関数をキャッシュしておく」という考え方

なるほど。イベント関数(になる予定の関数)を事前にbindAsEventListenerしておいて、変数に格納しておくわけですね。 そして、Event.observeでもEvent.stopObservingでも同じものを使うと。

いかがだったでしょうか? ちょっとは喉のつっかえが取れたような気持ちの方もいらっしゃるのではないでしょうか。

しかし、納得したとはいえ「なんでそんな面倒な仕様なんだよ」と思った方もいらっしゃるかもしれません。 setInterval()みたいにイベント関数IDみたいなやつをEvent.observe()で返してくれて、Event.stopObserving()ではそのIDを指定すればいいとかしてくれればいいんじゃないかと思いますけどね。

実際の利用現場では、多くのGUIやクリックエリアなどが登場しては消えたりするので、このキャッシュをオブジェクトに格納して運用するだけでも大変そうです。 個人的には要素自身のプロパティにキャッシュしておいて、要素を消したりしたい場合には設定していたプロパティの存在を確認して、登録してあったらstopObservingして削除すると良いと思います。 例をあげると以下のような感じです。

myElement.clickHandlerCache = obj.fx.bindAsEventListener(obj);
Event.observe(myElement, 'click', myElement.clickHandlerCache);

stopObservingしたければ、ルールにのっとってイベント関数のキャッシュを指定して解除

Event.stopObserving(myElement, 'click', myElement.clickHandlerCache);

要素を消す場合もclickHandlerCacheがあるかどうかを確認して削除します。

if (myElement.clickHandlerCache) {
    Event.stopObserving(myElement, 'click', myElement.clickHandlerCache);
    myElement.clickHandlerCache = null; //キャッシュをクリア
}
Element.remove(myElement);

    このエントリーをはてなブックマークに追加

このページに関連のある記事はこちら