DOM Events とブラウザの実装 ― 2007年03月23日 22時15分
ブラウザ上でのイベント処理の仕組みは DOM 2 Events および DOM 3 Events 草案にて規定されています。しかし、DOM 2 Events で言及されていない部分など、細かい動作はブラウザごとに異なっていることもあります。そうした仕様と実装の差異を、「作って納得! DOM 2 Events」で触れなかったものも含めて、いくつかまとめてみました。
ターゲットフェーズで呼び出されるリスナ
DOM 2 Events のイベントモデルにおいて、あるノードでイベントが発生すると、そのノードの祖先ノードのイベントリスナが呼び出されるキャプチャリングフェーズ、そのノード自身のイベントリスナが呼び出されるターゲットフェーズ、再び祖先ノードのイベントリスナが呼び出されるバブリングフェーズと、3 段階にわたってイベントが伝播していきます。このうちターゲットフェーズでは、addEventListener
メソッドで第 3 引数 useCapture
を false
に指定して登録したイベントリスナのみが呼び出されることになっています。
Opera 9 はこれに従っていますが、Firefox 2 および Safari 2 ではターゲットフェーズにおいて、useCapture
を true
に指定して登録したイベントリスナまでもが呼び出されてしまいます (参考: Mozilla Bug 235441)。なお、Safari はナイトリービルドにおいて一旦は仕様どおりの動作になったものの、Gecko との互換性を保つためにあえて以前の動作に戻したようです。
EventListener インターフェースを実装するオブジェクト
DOM 2 Events において、EventListener
インターフェースを実装するオブジェクトは handleEvent
メソッドを実装しなくてはいけません。しかし、これは Java のような、関数をファーストオブジェクトとして扱えない言語を考慮に入れてのことであり、付録の ECMAScript Language Binding によれば、ECMAScript では関数オブジェクトが EventListener
インターフェースを実装することになっています。すなわち、関数を addEventListener
メソッドの第 2 引数 listener
などに直接渡せるということです。
ですが、Firefox 2 や Opera 9 では、関数オブジェクトでなくとも handleEvent
メソッドを持つオブジェクトなら、EventListener インターフェースを実装するものとして扱ってくれます。Safari 2 では関数オブジェクトしか EventListener
として使えませんが、Safari のナイトリービルドでは handleEvent
メソッドを持つオブジェクトも EventListener
として使えるようです。
var listener = {
handleEvent: function (evt) { alert(this); },
toString: function () { return "[EventListener implementation]"; }
};
// Click, and alerts "[EventListener implementation]".
// OK: Firefox 2, Opera 9, and maybe Safari 3
element.addEventListener("click", listener, false);
イベントの伝播におけるルートノード
あるノードでイベントが発生すると、キャプチャリングフェーズではルートノードから直近の親ノードまで、バブリングフェーズでは親ノードからルートノードまでのイベントリスナが呼び出されます。ここで、ルートノードというのは、通常は文書ノード (document
) のことを指します。つまり、ある文書中の要素ノードでイベントが発生した場合、キャプチャリングフェーズは document
から始まり、バブリングフェーズは document
で終わるといった具合です。
Opera 9 はこのとおりの実装になっていますが、Firefox 2 および Safari 2 では window
オブジェクトがルートノードの役割を果たしています。すなわち、キャプチャリングフェーズは window
から始まり document
、ルート要素ノードと続いていき、逆にバブリングフェーズは window
で終わるのです。
複数のイベントリスナの実行順
DOM 2 Events では、addEventListener
メソッドで複数のイベントリスナが同じノード上に登録されたとき、それらをどういう順番で呼び出すかは規定されていません。しかし Firefox 2、Opera 9、Safari 2 はいずれも、登録されたのと同じ順番でイベントリスナを呼び出します。また、DOM 3 Events 草案でも、addEventListener
メソッドで登録されたイベントリスナは登録順で呼び出されることになっています。
イベント発生中のイベントリスナの編集
DOM 2 Events では、あるノードがイベントを処理している間にそのノードに追加されたイベントリスナは、その処理中には呼び出されないことになっています。ただし、イベントフェーズが異なれば呼び出されるので、キャプチャリングフェーズ中に追加されたリスナなら、バブリングフェーズでは呼び出されます。また、あるノードがイベントを処理している間にそのノードから削除されたイベントリスナは、決してその処理中に呼び出されることはありません。
Firefox 2 および Safari 2 はこれを仕様どおり実装していますが、Opera 9 ではイベント処理中に現在のターゲットノードから削除されたイベントリスナも呼び出されてしまいます。
イベントリスナ関数内の this の値
JavaScript では関数オブジェクトをそのままイベントリスナとして使えますが、これが呼び出されたときの関数内の this
が指す値は DOM 2 Events では規定されていません。Web Applications 1.0 には EventListener
インターフェースに関して In the ECMAScript binding itself, however, the
とありますが、これは handleEvent()
method of the interface is not directly accessible on Function
objects. Such functions, when invoked, must be called in the global scope.this
の値が window
オブジェクトになるということでしょうか?
実際には、Firefox 2、Opera 9、Safari 2 いずれも this
の値は現在イベントを処理中のノード、すなわち引数として渡されたイベントオブジェクトの currentTarget
プロパティの値と等しくなります。Firefox 2 ではテキストノード上のイベントリスナに関して、this
の値がイベントリスナ関数自身になってしまうバグがありますが、ナイトリービルドではすでに修正されています。また、Safari 2 では window
オブジェクト上のイベントリスナに渡されるイベントオブジェクトの currentTarget
プロパティの値が、キャプチャリングフェーズでは null
、バブリングフェーズでは document
オブジェクトになります。
余談ですが、上の引用をする際、Web Applications 1.0 の謝辞に Taken さんが載っていることに気づきました。一人だけ漢字なのでやたら目立ちますね。
createEvent メソッドの引数
イベントオブジェクトを作成するためには文書ノードの createEvent
メソッドを呼び出します。その際第 1 引数 eventType
に渡す文字列は、DOM 2 Events では "HTMLEvents"
、"UIEvents"
などとなっていますが、DOM 3 Events 草案では "Event"
、"UIEvent"
などに改められています (互換性のために DOM 2 Events の形式も認められています)。
Firefox 2、Opera 9 は DOM 2 Events および DOM 3 Events 草案の両形式に対応していますが、Safari 2 は DOM 2 Events の形式にしか対応していません。つまり、Safari 2 で基本的な Event インターフェースのみを実装するイベントオブジェクトを作成するためには、document.createEvent("HTMLEvents")
とする必要があります。なお、Safari のナイトリービルドでは DOM 3 Events の形式にも対応しているようです。
イベントオブジェクトの timeStamp プロパティ
DOM 2 Events において、Event
インターフェースの timeStamp
プロパティは、基準時刻からの経過ミリ秒数を表し、ECMAScript Language Binding ではその型は Date
型であるとされています。ただし、基準時刻ははっきりとは決められておらず、例としてシステムの開始時刻や Unix epoch (協定世界時 1970 年 1 月 1 日 0 時 0 分 0 秒) が挙げられています。DOM 3 Events 草案では基準時刻は Unix epoch であるとはっきり定められ、ECMAScript Language Binding での型も Number
型に変更されました。また、DOM 2 Events および DOM 3 Events 草案双方で、時間情報が利用できない場合は 0 を返すと規定されています。
各ブラウザの実装はというと、Firefox 2、Opera 9、Safari 2 のすべてにおいて timeStamp
プロパティの型は Number
型になっています。ただし、その値はばらばらで、Windows 版 Firefox 2 ではイベントにより、OS 起動時からの経過ミリ秒数を返すもの、0 を返すもの、どこから取ってきたのかわからない数値を返すものがあるようです。Windows 版 Opera 9 では常に 0 を返すようです。Safari 2 がどんな数値を返しているのかはよくわかりませんが、Unix epoch からの経過ミリ秒数ではないようです。
検証
以上の点を確認するためのサンプルを作りました。実際に各ブラウザでの違いを確かめてみてください。
コメント
_ mal ― 2007年03月24日 18時01分
_ nanto_vi ― 2007年03月31日 20時48分
http://www.quirksmode.org/dom/w3c_events.html
> http://d.hatena.ne.jp/quaa/20070116#p1
(Snip)
> に関しては「ターゲットフェーズで呼び出されるリスナ」でいいんですよね…
多分そうですね。ぜんぜん直ってません。
※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。
※投稿には管理者が設定した質問に答える必要があります。
トラックバック
このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2007/03/23/1339502/tb
http://d.hatena.ne.jp/quaa/20061107#p2
http://developer.mozilla.org/en/docs/Gecko_1.9_Changes_affecting_websites
-> Capturing load event listeners
あたりもふれてほしいです。
http://d.hatena.ne.jp/quaa/20070116#p1
-> 「EventTargetに直接イベントが送られたときにそのEventTargetについたキャプチャ用イベントリスナが誤発動する問題とかは直ってるのかな」
に関しては「ターゲットフェーズで呼び出されるリスナ」でいいんですよね…よくわかってなくてすいません。