[追記 dateTime="2007-12-07 夕刻"]あーっ、今ごろ気付いた。イライライって「イ」がひとつ多いや。タイトルを編集すると、なんか悪いことが起こったりしませんかね? うーん、いいや。typoしたままにしとこう。ちなみに、「イライライ」は回文になっているぞ。[/追記][追記 date = "2007-12-11"]http://d.hatena.ne.jp/keyword/%a5%a4%a5%e9%a5%a4%a5%e9%a5%a4 [/追記]
いつか文句言ってやろうと思っていた件ですよ。長いぞ。
内容:
- 似てるけど少しずつ違うイベントモデル達
- イベントターゲット
- イベントフロー
- EventTargetインターフェース
- イベントハンドラーとイベントリスナー
- リスナーとハンドラーについてもう少し
- イベント伝搬とハンドラー実行
- イベントの通過または出現
- イベントタイプ
- 「イベントが…」「イベントを…」
短いまとめとして、「早わかり イベントモデル」を書きました。
似てるけど少しずつ違うイベントモデル達
ブラウザーのJavaScriptや、Flash PlayerのActionScriptにおいて、DOMレベル2のイベントモデルの採用が進んでいます。これは喜ばしい事態なんですが、DOMレベル2のイベントモデル(以下、単にDOMイベントモデル)ってのが、なんだかちょっと混乱しているんだよね。
JavaのAWTやSwingにもイベントモデル(以下、Javaイベントモデル)がありますよね。JavaイベントモデルとDOMイベントモデルは似ているんだけど同じじゃない、よく見れば随分と違う。こういう「似てるけど違う」のは紛らわしくて困りますよ。
さらに、W3C仕様として、XML Events(以下、XMLイベント)ってのもあって、これはDOMイベントと密接な関係があるのだけど、イベント処理を記述するマークアップ構文を主に定義してます。DOMイベントのほうは2000年に勧告、XMLイベントは2003年に勧告になっています。XMLイベントのほうが後から作られたので、DOMイベントの変なところや曖昧さを修復する努力がなされています。しかし一方、概念や用語をさらに増やしてしまったのも事実で、混乱が収拾したとは言いがたいですね。
とまー、そんな状況に僕はけっこうイライラしてるのですが、文句を付けつつも解説と整理を試みてみましょう。
以下の話で、DOMイベントとXMLイベントは別な仕様ですから混同しないように十分注意してください。DOMイベントもXMLイベントも、より新しい仕様(レベル3とバージョン2)の策定作業が進んでますが、勧告(Recommendation)のほうをベースにします。
- http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113(勧告)
- http://www.w3.org/TR/2003/REC-xml-events-20031014 (勧告)
- http://www.w3.org/TR/2006/WD-DOM-Level-3-Events-20060413 (レベル3 草稿)
- http://www.w3.org/TR/2007/WD-xml-events-20070216(バージョン2 草稿)
イベントターゲット
イベントとは、なんらかの出来事(happening)を通知するために使うデータのことだとしましょう。具体的な例としては、マウスを動かしたりマウスボタンを押したときに発生するマウスイベントがあります。イベントを引数に取り処理を行う関数やメソッドはイベントハンドラー(あるいは単にハンドラー)と呼ぶことにします。マウスイベントを引数にして処理する関数がマウスイベント・ハンドラー(あるいはより短くマウスハンドラー)です。
[追記]マウスに付いているハードウェアとしてのボタンと、画面上に表示される視覚的部品であるボタンの区別がハッキリしなかったので、「マウスボタン」「ボタン・コントロール」と表記し分けることにしました。[/追記]
Javaでは、イベントの発生源のオブジェクトをイベントソースと呼びます。例えば、画面上のボタン・コントロールがクリックされてマウスイベントが発生したら、そのボタン・コントロールが当該マウスイベントのソースになります。発生源をソース(source)と呼ぶのは自然ですよね。
ところがDOMイベントモデルでは、イベントの発生源をなんとイベントターゲットと呼びます。ええええーっ、ソース(出発点)とターゲット(到着点)じゃ意味が逆じゃん、なにそれ?
僕の推測ですが、たぶん以下のような理由です;
ボタン・コントロールのクリックの例で、ソフトウェア的なイベント発生源はボタン・コントロール(を表現するオブジェクト)と考えてよいでしょう。でも、ホントの発生源はハードウェアデバイスとしてのマウスですよね。DOMイベントでは、ホントの発生源からマウスイベントを受け取るのはDOM実装系だと考えます。ここでは、「DOM実装系=ブラウザ」としましょう。ブラウザは、画面上のクリック位置から適切なDOMノード(<input>や<button>というHTML要素と思ってもいいです)を探し当てます。このノードがターゲットノードです。
ブラウザから見れば、イベント(この例ではマウスイベント)を送り届けるべき相手なので「ターゲット」ってわけ。ターゲットへとイベントを送出(発送)する操作をディスパッチ(dispatch)と呼びます。世の中にあるイベント処理のソースコードを見ていると、dispatch, process, fireなんて動詞がよく使われていますが、恣意的<しいてき>に使われていて別に統一性はありません。しかし、今ここに出てきたディスパッチにはハッキリとした定義があります(詳細は後述)。
ターゲットノードに向けてディスパッチ(送出)されたイベントは、イベントフロー(event flow)と呼ばれる旅に出ます。
イベントフロー
イベントフローは、こんな図で説明できます(http://www.w3.org/TR/xml-events/images/event-flow.png)。
DOMツリー構造のなかの、特定されたターゲットノードに対して、まずルートノード(文書ノード*1)から葉(リーフノード)の方向にイベントがくだっていき、ターゲットノードで折り返して、ルートまで戻ります。この往復運動の「行き=くだり」をキャプチャリング・フェイズ*2、「戻り=のぼり」をバブリング・フェイズと呼びます。([追記]ターゲットノードがリーフノードとは限らないので注意して下さい。ターゲットがルートノードであることさえあります。[/追記])
と説明しましたが、「問題あり」なんですよ。まず、イベントフローと紛らわしい概念がいくつかあります。ツリー内でのルートノードからターゲットノードに至るノード列、これはイベント伝搬の道(path*3)とかチェイン*4と呼ばれています。イベントフローは、ノードのチェインのことではなくて、チェイン上を往復する運動過程、行為のことなんです。じゃぁ、イベントフローとイベント伝搬(propagation)は違うのか? たぶん同じだと思いますよ、確信はないけど。
今さっき、フェイズがキャプチャリングとバブリングの2つあるように説明しましたが、DOMイベント仕様のIDL(プログラミング言語中立なインターフェース定義)を見ると*5、
/* interface Event より */ // PhaseType const unsigned short CAPTURING_PHASE = 1; const unsigned short AT_TARGET = 2; const unsigned short BUBBLING_PHASE = 3;
と、フェイズは3つあります。実は、「バブリング・フェイズ」って言葉に狭義と広義があって、狭義の(厳密な)バブリング・フェイズは、ターゲットノードを除いたチェインを昇る(ルートに向かう)過程です。広義の(いいかげんな用法の)バグリング・フェイズは、ターゲットノード自身も含めたチェイン上での運動(伝搬)のことです。
XMLイベント構文のphase属性では、「キャプチャ」と「バブル」ではなくて、「キャプチャ」と「デフォルト」(capture, default)という言葉を使っています。キャプチャリング・フェイズは使用しないことが多いので、ターゲット・フェイズ+狭義のバブリング・フェイズを「デフォルトのフェイズ」と呼ぶのは妥当だと思います。
EventTargetインターフェース
DOMイベントのIDLには、EventTargetというインターフェースが定義されています。
interface EventTarget { void addEventListener(in DOMString type, in EventListener listener, in boolean useCapture); void removeEventListener(in DOMString type, in EventListener listener, in boolean useCapture); boolean dispatchEvent(in Event evt) raises(EventException); };
このEventTargetは、先に説明した「イベントフローの折り返し点としてのイベントターゲット」とはまた別なのです。イベントターゲットになり得るノード(より一般にはオブジェクト)が実装すべきインターフェースがEventTargetです。
イベントフローのチェイン上のすべてのノードはEventTarget(正確に言えば「EventTargetインターフェースを実装したノード」)です。となると、イベントターゲットはもちろんEventTargetですが、他のノードだってみんなEventTargetです。イベントが、イベントターゲットではないEventTarget上にいるときでも、そのEventTargetをターゲット(current target)と呼んじゃったりします。って、わかりますか? なんかもう、用語法、無茶苦茶ですな。
XMLイベント仕様では、「イベントターゲット、EventTarget」という言葉の過剰かつ異常なオーバーロードを緩和するため、イベントフローのチェイン上のノードをオブザーバーと呼んでいます。ターゲット(ホントのターゲット!)はもちろんオブザーバーです。
オブザーバーといえば、Observerパターン(Publish-Subscribeパターン)を思い起こす人が多いでしょう。が、XMLイベントのオブザーバーは、ObserverパターンのObserverではなくてSubject(Publisher; observeされる側)のほうです。ObserverパターンのObserverは、Javaならリスナー(listener)なんですが、XMLイベントにおけるリスナーはまた別な概念です(後述)。いやはや、なんとも。
ActionScript 3.0では、EventTargetの代わりにEventDispatcher(インターフェース名はIEventDispatcher)を使っています(http://livedocs.adobe.com/flash/9.0_jp/ActionScriptLangRefV3/flash/events/IEventDispatcher.html)。個人的にはピンと来ませんが、ここらへんが無難な呼び方でしょうか。
イベントハンドラーとイベントリスナー
イベントリスナーという言葉もいろいろな場面でよく目/耳にしますが、イベントハンドラーと違うのでしょうか? イベントリスナーとイベントハンドラーは同義語として使うことが多いようですが、Javaイベントモデルのリスナーは単一のハンドラーメソッドではなくて、いくつかのハンドラーメソッドを備えたオブジェクト、あるいはハンドラーメソッドの集合体という感じでしょう。
DOMイベントモデルでは、リスナーとハンドラーの区別は曖昧なままです。一方、XMLイベント仕様では、ちゃんと区別して定義しています。曰く:
A listener is a binding of such a handler to an event targeting some element in a document.
リスナーとは、a binding of an event handler to an observer -- つまり、イベントハンドラーとオブザーバー(ノード)の束縛(対応付け)なのです。なんだか随分無理矢理な感じですが、この定義でいちおうツジツマ合わせは出来ます。
まず、大域的関数である addEventBinding(Node observer, String eventType, EventHandler handler) を考えます。この関数は、あるノード(オブジェクト)に対して、eventTypeで識別される種類のイベントが来たときの処理関数handlerを対応付けます。大域関数を、ovserverのメソッドと考え直すと、observer.addEventBinding(String eventType, EventHandler handler) となります。XMLイベントのお約束(定義)で、bindingとlistenerは同義語ですから、observer.addEventListener(String eventType, EventHandler handler) となります。(useCapture引数は省略しました。)
XMLイベントは、この「listener = binding」のコジツケでなんとか押し通しているのですが、DOMイベントでは、
interface EventListener { void handleEvent(in Event evt); };
なんて定義があるので、後付けの「listener = binding」で強引に解釈することは難しく、やっぱり「listener = handler」と考えざるを得ないのです。それにしても、上記の定義は、リスナーとハンドラーの混乱状況をよく表しています。名詞がlistenerで動詞がhandleですもん。
interface EventHandler { void handleEvent(in Event evt); };
としておけば何とかなったのに、、、後悔先に立たず。
悪のりして「時間が戻せるなら」の話を付け加えるなら、イベントに反応可能なオブジェクトを EventResponder とでも呼んで、
interface EventResponder { void addEventBinding(in DOMString type, in EventHandler handler, in boolean useCapture); void removeEventBinding(in DOMString type, in EventHandler handler, in boolean useCapture); boolean dispatchEvent(in Event evt) raises(EventException); };
のようなネーミングにしておけば、あちこち角が立たずに済んだような気がしますが、まー後知恵でゴチャゴチャ言うのもあさましいから、このへんにしときます。
リスナーとハンドラーについてもう少し
僕の推測(邪推?)ですが、
interface EventListener { void handleEvent(in Event evt); };
この定義を合理化する論理はたぶん次のようなものでしょう;
Javaでは、イベントハンドラーメソッドの集合がイベントリスナーです。リスナーが、たまたまただ1つのハンドラーしか含まないこともあります。例えば:
// 構文はJavaではなくてIDL interface ActionListener { void actionPerformed(in ActionEvent evt); };
先のEventListenerの定義は、1個しかハンドラーメソッドを含まないリスナーインターフェースだと考えればいいのです。
これは、Java的な観点からは納得できるんですが、IDLはプログラミング言語中立な定義のはずです。JavaScriptやActionScriptではこういう論法は通用しません。実際、JavaScript/ActionScriptでは、EventListenerは関数(への参照)として実現されます。handleEventというメソッドを持つオブジェクトではなくて、ハンドラー関数そのもの(クロージャ)がEventListenerなんです。
IDL上に存在していたhandleEventという名前は、JavaScript/ActionScriptによる実装では消えてしまいます。そんなことなら、
// listenerとhandlerが同義語だとして interface EventListener { void apply(in Event evt); };
とでもしておけば、まだしも筋を通せたでしょうに(あっ、また後知恵の愚痴でした)。
これはまた、クロージャや高階関数をうまく記述できないIDLの限界が露呈している、ってことかもしれません*6。
イベント伝搬とハンドラー実行
ここでまた、「ソース」と「ターゲット」の問題を考えてみましょう。同じ概念に、なぜ正反対の呼び名が付いてしまったのでしょうか。
イベントのディスパッチとか配信(delivery)と言う場合に、異なる2つの意味があります。
- 構造化されたオブジェクト群のなかをイベントが伝搬する過程。
- 特定のオブジェクトにイベントが到着したとき、そのイベントにマッチしたハンドラ達が実行される過程。
DOMイベントモデルでは1番目のイベント伝搬が強調され、Javaイベントモデルでは2番目のハンドラー実行に焦点が当てられるのです。
どんなイベントモデル/イベントシステムでも、イベント伝搬とハンドラー実行の両方が必ずあります。しかし、DOMイベントモデル以前の多くのイベントモデルでは、イベント伝搬機構はシステムに組み込みで、プログラマがあまり手出しできなかったのです。よって、プログラマから主に見えているのは、特定オブジェクトからのハンドラー実行機構です。そのため、例えばボタン・コントロールがクリックされたことに対して、
- マウスのクリックイベントがボタン・コントロールに届くまでの伝搬過程に注目。ボタン・コントロールは、イベントの配送目的地だ。
- ボタン・コントロールから、そこに登録されたイベントリスナー・オブジェクトにイベントが委譲(デリゲート)される過程に注目。ボタン・コントロールは、委譲によるハンドラー実行の発生源だ。
となり、「ターゲット」と「ソース」という呼び名になるわけです。
過去の多くのイベントモデル/イベントシステムにおけるディスパッチは、単にハンドラーの選択と実行のことでしたが、DOMイベントモデルにおけるディスパッチは、イベント伝搬(イベントフロー)と、各ノードにおけるイベント実行の両方を含んだ概念です。
DOMイベントモデルでは、各ノードにおけるイベント実行を「イベントがリスナーをトリガーする」と表現しているようです(XMLイベントでは「活性化する」がよく使われています)。ここでも用語「トリガー」を使うことにします。
イベントの通過または出現
イベントフローの折り返し点であるイベントターゲットは例外として、他のオブザーバーノードでは、行き(キャプチャリング・フェイズ)と戻り(バブリング・フェイズ)の2回イベントが通過(pass through)します。XMLイベントの用語法では、イベントの通過を「イベントが出現(occur)する」とも言っています。
特定ノードにへばりついた虫の立場になると、ある時イベントが出現し、しらばくして同じイベントがもう一度出現するように見えるでしょう。この通過、または出現のときに、イベントはノードに関係づけられた(束縛された)ハンドラーをトリガーしていくのです。
もちろん、ノードに登録されているすべてのハンドラーがトリガーされるわけではなくて、出現したイベントとマッチするハンドラーだけがトリガーされるのです。このマッチングの目安となるのがイベントタイプです。
イベントタイプ
DOMイベントのタイプとは、getType()メソッドまたはtypeプロパティ(IDLレベルでは、readonly属性type)で取得できる文字列です。例えば、"click" とか "mouseDown" という文字列がイベントタイプを示すのですね。そして、addEventListener(type, handler, useCapture)のtypeとしてこの文字列を使います。生の文字列を書いたりすると間違いやすくて危ないので、定数定義を使うのが普通です。例えば、ActionScript 3.0 なら、flash.events.MouseEventクラス内に、
CLICK : String = "click" MOUSE_DOWN : String = "mouseDown" MOUSE_OVER : String = "mouseOver"
のような定数定義があるので、addEventListener(MouseEvent.CLICK, clickHandler, false) のように使います。
Javaのイベントモデルの場合は、静的で厳密な型システムを利用することにより、イベントリスナーの定義や登録が安全に行えたのですが、DOMイベントのタイプは文字列の名前ですからあんまり安全ではありません。
もっともJavaでも、AWTEventのサブクラスでは、getType()とほとんど同じgetID()(こちらは整数を返す)もあって、MouseEvent.MOUSE_CLICKED のような定数値も使っています。そしてまた、DOMイベントに型階層がまったくないわけでもなく、Event → UIEvent → MouseEvent のような型階層があります。
ひょっとすると、λh.addEventListener("click", h, false) : (MouseEvent -> Void) -> Void のような型宣言を解釈できるプログラミング言語なら、ハンドラー登録の型安全性をコンパイル時に保証できるかも知れません(よくわからんけど)。
話を戻すと; addEventListener("click", h, false) として登録されたイベントハンドラーhは、evt.type == "click" であるイベントevtによりトリガーされるのです。イベントがイベントフローによりDOMツリーの上を行き帰りするとき、その途中で通ったノードに登録されたハンドラーを次々とトリガーしながら動くことになります。
「イベントが…」「イベントを…」
イベント関係の資料を読んでいるとき、「イベントが…」「イベントを…」といった意味で使われる動詞「…」として、
dispatch, distribute, deliver, propagate, listen, observe, process, handle, trigger, fire, invoke, execute, activate, occur, happen, pass through, issue, cast, post, send, generate, produce(順不同)
なんてのが出てきました。
多くの場合、これらが正式なテクニカルタームなのか、説明のためにたまたま使った言葉なのか(少なくとも僕には)わからないことに困惑しました。言葉にばかり神経質になってもしょうがないのですが、概念を抽出し鮮明に描写し伝達するには、言葉を使う以外に方法がないのもまた事実です。
イベントモデル達の概要をまとめてみて、「用語の選択と運用を、もう少し注意深く厳密にして欲しかったよな」というのが僕の感慨です。
[追記]以上の内容を、手短に整理したい方は「早わかり イベントモデル」をどうぞ。[/追記]
*1:ブラウザー内のオブジェクト階層のルートはwindowオブジェクトですが、windowオブジェクトはDOM仕様の範囲外なので、文書ノードをルートとしていいと思います。
*2:W3Cの文書内でも、"capture phase"と"capturing phase"が混在してます。
*3:pathという言葉はXMLイベント仕様でしか使われていません。
*4:the chain of EventTarget's はDOMイベント仕様の用語です
*5:もちろん、本文にも書いてあります。
*6:IDLはもともと、分散アプリケーションのインターフェース記述の目的で作られたものです。ネットワーク内に関数(を表すデータ)を流すことは考えにくいので、高階関数は考慮されなかったのでしょう。ローカルAPIの記述まで守備範囲を拡げて、IDLを拡張する必要がありそうですね。