GeckoのDOMイベント処理の実装 (Gecko Inside ver.)

Preview:

Citation preview

中野雅之

•肩書き

•正式: Mozilla Japan 国際化担当マネージャ

•非公式: Mozilla Japan 大阪支部長

•大阪の自宅で、自宅警備しながら仕事してます

中野雅之

•色んなアカウント

•メールアドレス: masayuki@d-toybox.com

• Skype: masayuki-nakano

• Twitter: @d_toybox

• Blog: 「もずはっく日記」で検索

アジェンダ

•簡単な用語解説

• DOMイベントの基礎知識

• Geckoのイベント処理の実装

•イベントとe10s

簡単な用語解説

• D3E

• DOM Level 3 Events の略

• DOM Level 4 Events にあたる仕様案の名前だった、”UI

Events” に改名され、マージされた

• DOM Level 3 KeyboardEvent key Values

• KeyboardEvent.keyの値を定義した仕様

• 元々は D3E で定義されていたものの分割された

• DOM Level 3 KeyboardEvent code Values

• KeyboardEvent.codeの値を定義した仕様

• 元々は UI Events で定義されていたものが分割された

簡単な用語解説

• Edit Events

• UI Events からinputイベントとbeforeinputイベントを分離した仕様

• しかし、迷走中

• inputイベントとbeforeinputイベントはそれぞれ、editとbeforeeditイベントに改名され、従来のinputイベントとの後方互換性も無い

簡単な用語解説

• PresShell

• “Presentation Shell”の略

• DOM document (window?)ひとつに対して、一つ生成される

• nsPresShell.cpp で PresShellとして実装され、nsIPresShell抽象クラスでインターフェースが定義されている

• ESM

• イベントの前処理や後処理、イベントによって変化するコンテンツの状態管理(:hover状態等)を管理している、EventStateManagerの略

• PresShellに生成され、破棄される

簡単な用語解説

• PresContext

• “Presentation Context”の略

• PresShellに生成され、破棄される

• RefCountable ではないので、クラスのメンバとして保存する時は nsCOMPtr<nsIPresShell>を保存しておき、GetPresContext()で取得する方が安全

簡単な用語解説

• dispatch、fire

•共にイベントを発火する意味で違いは無い

• default action

•イベントが発生した際にブラウザが行う動作、例えば、wheelイベントに対する、スクロールやズーム処理がwheelイベントのdefault action

• consumed

• Event.preventDefault()が呼び出され、default action

がキャンセルされている状態

DOM イベントの基礎

• フェイズと、プロパゲーション

• イベントを捕まえる

• イベントの伝播を抑制する

• ブラウザのイベント処理を妨害する

• Gecko独自のイベントグループ

capturing フェイズ

1. 最初はwindow

2. 次にdocument

3. rootからtargetの親

• targetの決定は、イベント依存

• フォーカス• 発生位置• ……等々

target フェイズ

• targetフェイズは、capturingフェイズの一部でもあり、bubblingフェイズの一部でもある。

• windowやdocumentがtargetの場合も

bubbling フェイズ

1. target の親からroot

2. 次にdocument

3. 最後にwindow

• Event.bubblesが falseの場合、bubblingは発生しない

イベントを捕まえる

• 書式:

• EventTarget.addEventListener(イベント名, ハンドラ, Capture?);

• EventTarget.removeEventListener(イベント名, ハンドラ, Capture?);

• イベント名: イベントの名前 "click" とか、 "keydown"

• ハンドラ: function foo(aEvent) { /* something */ }がお手軽で十分

• Capture?: trueなら、capturing フェイズと、target フェイズ、falseなら、target フェイズと、bubbling フェイズ

イベントを捕まえる

• 単純な例1:

• 全ての要素で発生するkeydownイベントを知りたい

• イベントターゲットより先に知りたい

• removeする必要はない

document.addEventListener("keydown",function (aEvent) { /* something */ },true);

イベントを捕まえる

• 単純な例2:

• 全ての要素で発生するkeydownイベントを知りたい

• イベントターゲットより後に知りたい

• removeする必要がある

function keydownHandler(aEvent) {/* something */

}

document.addEventListener("keydown", keydownHandler, false);

document.removeEventListener("keydown", keydownHandler, false);

イベントの伝播を抑制する

• 複数のイベントリスナを登録している場合、あるイベントハンドラで処理済みのイベントを他のイベントハンドラでは無視したい場合が考えられる

• Event.stopPropagation()か、Event.stopImmediatePropagation()を呼ぶと、それ以降のイベント発生を中止できる

• Event.stopPropagation()では、次のEventTarget以降でのイベントハンドラの実行を中止できる

• Event.stopImmediatePropagation()では、同じEventTargetに登録されている未処理のイベントハンドラも含めて、イベントハンドラの実行を中止できる

ブラウザのイベント処理を妨害する

• ブラウザにdefault actionを実行してもらいたくない場合が考えられる

• Event.preventDefault()を呼び出すと、Event.defaultPrevented属性値がtrueになる

• defaultPrevented属性がtrueなら、ブラウザはdefault action

を実行しない(ただし、タブの切替等、セキュリティ上、無視することはある)

• Webアプリは Event.defaultPreventedの値を、処理前に確認することで、Event.stopPropagation()の代用としても使える

Gecko独自のイベントグループ

• GeckoはDOMイベントを捕まえることでデフォルトアクションを実装することがある

• WebコンテンツがEvent.stopPropagation()を呼び出したものの、Event.peventDefault()を呼び出していない場合にdefault

actionを実行する必要がある

• Webコンテンツと同じ様にイベントリスナを登録していると、Event.stopPropagation()の呼び出しだけでdefault actionの実行が抑制されてしまう

• 実はこのバグは多く、私自身も発見しては修正している

Gecko独自のイベントグループ

• GeckoはWebコンテンツの登録するイベントリスナをdefault

event groupに登録する

• Gecko内部や、UIはイベントリスナをSystem Event Groupに登録する(べき)

• 最初にdefault event groupに登録されたリスナが実行され、その後、system event groupに登録されたリスナが実行される、つまり……

Gecko独自のイベントグループ

1. default event groupのcapturing phase

2. default event groupのtarget

3. default event groupのbubbling phase

4. system event groupのcapturing phase

5. system event groupのtarget

6. system event groupのbubbling phase

という順序で実行される

Gecko独自のイベントグループ

• Event.stopPropagation()やEvent.stopImmediatePropagation()は各event group内での伝播を止めるのみ

• default event groupで呼び出していても、system event

groupでの実行は保証されている

• system event group内で呼び出した場合にはdefault event

groupと同様にそれ以降のイベントリスナは呼び出されないのでsystem event groupに登録しているイベントリスナ全てが実行を保証されている訳ではない

Geckoのイベント処理の実装

• OSネイティブなイベントをDOMイベントとしてWebアプリに通知するケースを解説

•図にすると、次のように……

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

EventListenerManager

• イベントターゲットで、最初にイベントリスナが登録された時に、インスタンスが生成され、そのノードに保存される

• そのノードに登録された全てのイベントリスナを管理する

• HandleEvent()が呼び出されると、イベントの名前とフェーズ、イベントリスナのグループがマッチするものを探し、マッチした場合に登録されているハンドラを実行する

• stopImmediatePropagation()が呼び出されたら、中断する

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

モジュール間の処理の流れの例

1. widget/ は、各 OS のネイティブイベントをハンドリングするモジュール。ネイティブイベントが発生したら、内部イベントである、Widget*Eventクラスのインスタンスをスタックに作成し、その値を設定する

2. PresShellにイベントを送信、PresShellが target を決定する

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

モジュール間の処理の流れの例

3. EventStateManager::PreHandleEvent()が前処理を行う

• 後ほど詳しく……

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

モジュール間の処理の流れの例

4. PresShellがEventDispatcher::Dispatch()を呼び出す

5. EventDispatcher::CreateEvent()でWidget*Eventのクラスに対応した、dom::*Eventクラスを選択し、そのインスタンスをヒープに作成する

6. EventListenerManagerを列挙した配列を用意し、EventTargetChainItem::HandleEventTargetChain()を呼び出す

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

モジュール間の処理の流れ

7. EventTargetChainItem::HandleEventTargetChain()で、受け取った、各EventListenerManagerのHandleEvent()を実行していく

8. EventListenerManager::HandleEvent()は、そのノードに登録されたイベントリスナを順に実行していく

9. EventTargetChainItem::HandleEventTargetChain()が自身をもう一度呼び出し、デフォルトアクションの実行のため、System Event Groupに登録されたリスナを実行する。

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

モジュール間の処理の流れ

10. EventStateManager::PostHandleEvent()で、デフォルトアクションを実行したり、関連するイベントを生成したりする

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

Widget*Event、Internal*Event

• WidgetEventクラスが全てのイベントクラスの基底クラス

• WidgetMouseEventや、InternalFocusEventといった派生クラスが、イベントごとに定義・実装されている

• 各クラスは、ほぼ構造体であり、メンバのカプセル化はほとんど行われず、メンバにダイレクトにアクセスする (読み書き問わず)

• widgetから生成されるイベントは、Widget*Event、それ以外の場合は、DOMイベントのデータを保存するためだけのものは、Internal*Event と命名されている

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

PresShell

• イベントの複雑な処理はEventStateManager、表示に関わる部分はnsPresContextに分離されている

• イベント処理では widget で生成されたイベントを受け取り、target の決定と、発火処理を行う

• PresShell::HandleEvent()や、関連メソッドを参照

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

EventStateManager::PreHandleEvent()

• PresShellがイベントを発火する直前に呼び出す

• マウスの操作によって変化する:hoverや、:activeといった要素の状態設定を行う

• マウスのクリックカウント等、状態の保存や、その値のWidget*Eventのメンバへの書き出しを行う

• ホイールの速度等、設定でカスタマイズできる項目を、Widget*Eventのメンバの値を書き換えることで実現してる

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

EventDispatcher::CreateEvent()

• Widget*Eventクラスのインスタンスや、Internal*Eventクラスのインスタンスから、DOMイベントの実装クラスを選択し、インスタンスを生成する

• Javascriptのdocument.createEvent()の実装でもある

• new MouseEvent(…)等は別

• EventListenerManager::HandleEventInternal()が最初のイベントリスナを見つけた時点で呼び出される

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

dom::Event

• DOMイベントの実装クラスは、名前空間 “mozilla::dom“に、標準仕様と同じ名前で定義

• dom::KeyboardEvent、dom::WheelEvent等

• nsIDOMEventインターフェースを必ず継承している

• dom::Eventは、WidgetEventへのポインタを持ち、イベントの属性値はここに保存している

• DOMイベントクラスは、Widget*Eventや Internal*Eventへ、DOMからアクセスするためのラッパクラス

dom::Event

• 古くから実装されているDOMイベントクラスは、固有の、nsIDOM*Eventインターフェースも継承している

• 新規作成時にはほとんど定義されない

• データにアクセスするだけなら、内部イベントクラスにアクセスする方が高速で、実装も単純化される

• nsIDOM*Eventのメソッドは全て、virtual callになるためパフォーマンスが悪い

dom::Event

•内部イベントクラスを定義・実装する必要が無い、シンプルなイベントを実装する際は、webidlで定義したイベントから、実装コードを自動生成することも可能

•自動生成の定義は dom/webidl/moz.build で定義

• dom/bindings/Codegen.py によって、ビルド時にcppファイルが自動的に生成される

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

EventDispatcher::Dispatch()

• PresShellから呼び出される

• 最初に、 targetのPreHandleEvent()を実行する

• windowからtargetノードまで辿り、作成されている全てのEventListenerManagerを列挙し、配列に格納

• EventTargetChainItem::HandleEventTargetChain()にその配列を渡す

• その後、 targetのPostHandleEvent()を実行する

• 発火が終了しても、DOM イベントが JS の変数等から参照されている場合、内部イベントをヒープにコピーする

• 発火処理後、スタックにある内部イベントは破棄されるため

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

EventTargetChainItem::HandleEventTargetChain()

• EventDispatcher::Dispatch()に呼び出される

• 列挙されている、windowからtargetの親ノードまでのEventListenerManagerの、HandleEvent()を実行していく(capturingフェーズ)

• Event.stopPropagation()がHandleEvent()実行中に呼び出されていたら、ループを中断する

EventTargetChainItem::HandleEventTargetChain()

• 次に、targetのEventListenerManagerのHandleEvent()を呼び出す (targetフェーズ)

• Event.stopPropagation()か、Event.stopImmediatePropagation()が既に呼び出されていた場合、実行しない

• Event.stopPropagation()がHandleEvent()実行中に呼び出されていたら、ループを中断する

EventTargetChainItem::HandleEventTargetChain()

• 次に、列挙された、targetの親から、windowまでのEventListenerManagerの、HandleEvent() を実行していく(bubbling フェーズ)

• Event.bubblesがfalseなら実行しない

• Event.stopPropagation()か、Event.stopImmediatePropagation()が既に呼び出されていた場合、実行しない

• Event.stopPropagation()がHandleEvent()実行中に呼び出されていたら、ループを中断する

EventTargetChainItem::HandleEventTargetChain()

• 次に、イベントターゲットが生成した、CSS boxを実装している、nsIFrame::HandleEvent()を呼び出す

• PresShellがEventDispatcher::Dispatch()に渡したcallbackクラス経由で呼び出し

• default actionの実装に利用可能

EventTargetChainItem::HandleEventTargetChain()

• 最後に、System Event Groupのイベントリスナが登録されている場合、capturing、target、bubblingフェーズを再度、処理する

• 通常のWebコンテンツからは登録不可

• Web コンテンツにEvent.stopPropagation()等を呼び出されていても実行される

• default actionの実装に利用可能

• System Event Group 内のリスナで、stopPropagation()等を呼ぶと中断される

Geckoのイベント処理の実装nsDocument

PresShell

widget EventDispatcher

EventStateManager

Widget*Eventdom::*Event

PreHandleEvent() PostHandleEvent()

Dispatch()

CreateEvent()

2

3

4

5

6

EventTargetChainItem

nsINode

EventListenerManager

HandleEvent() handler2handler1

nsINode

EventListenerManager

HandleEvent() handler2handler1

7

810

1

9

EventStateManager::PostHandleEvent()

• PresShellが最後に呼び出す

• イベントの後処理や、default actionを実行

• マウスホイールによるスクロール処理やズーム処理

• mouseupからclickイベントや、dblclickイベントの生成

• mousemoveから、 mouseover、mouseout、mouseenter、mouseleaveイベントの生成

イベントとe10s

PresShell

widget

EventStateManager

PostHandleEvent()

1

2 5

6

TabParent

3 4

PuppetWidget

7

PresShell

9

EventStateManager

EventDispatcher

etc.

11

12

TabChild

8

10

13

イベントとe10s

• e10sではネイティブのイベント入力は全てchromeプロセスで処理し、Widget*Eventをwidget/からdispatchする

• フォーカスを持った要素が子プロセス内にある場合、EventStateManager::PostHandleEvent()がイベントをTabParent経由で子プロセスへ送信する

• 一部例外のイベントもあり(WidgetCompositionEvent等)

• 子要素ではTabChildがイベントを受け取ると、PuppetWidgetへ送信し、PuppetWidgetから子プロセス内のPresShellにイベントを送信し同様に処理される

• つまり……

イベントとe10s

• 子プロセスに送信されるイベントは、親プロセスのdefault

actionの一部

• 子プロセスには非同期で送信されているため、親プロセスはそのイベントがconsumedとなったかどうかを知ることができない

• OS側に非同期でイベントの処理結果を返す方法が無い限り、嘘の結果しか返せない

• 各OSがモダンな結果になることを期待したい

• IMEのみ、即座にコンテンツの情報を返したりする必要があるため、ContentCacheという仕組みを用意している

Text

Q&A?

Recommended