❗
Reactとブラウザのイベントシステムの違い
はじめに
Reactでイベントに対する処理を実装することはよくあるかと思います。Reactのイベントシステムとブラウザのイベントシステムの違いについて正しく理解していなかったため、今回はその違いについてまとめました。
そもそもイベントとは
例えば、ウェブサイトにおいてボタンがクリックされたときや、テキストフィールドに入力があった際に発生するもの、それがイベントです。以下は、ウェブ上で遭遇する一般的なイベントの一覧です。
また、イベントの発生に対してアクションを行う際は、イベントリスナーと イベントハンドラー を使います。イベントリスナーはイベントが発生した際に実行される関数を登録する仕組みを指します。イベントハンドラーはイベントが発生した際に実行される関数のことを指します。
ブラウザのイベントシステム
1. 処理の登録方法
以下の二通りがあります。
- addEventListnerを使う方法
el.addEventListener("click", someFunction);
- onclickなどのプロパティに対してイベントハンドラーを渡す方法
<button onclick="alert('button クリック')">クリック</button>
2. イベントオブジェクト
イベントが発生すると、イベントオブジェクトが生成されます。以下のように、イベントハンドラーの中で取得することができ、中にはイベントの種類やイベントが発生した要素の情報などが含まれています。
<button onclick="(e) => console.log(e.target)">クリック</button>
3. イベントの伝搬
- イベントが発生した要素からその要素の親や祖先要素に、イベントが伝わるプロセスを指します。
- 例えば、以下のコードの
button
をクリックした場合、button
の親であるdiv
にもクリックイベントが伝搬します。そのため子
→親
の順でalert
が呼び出されます。
<div onclick="alert('親')">
<button onClick="alert('子')">クリック</button>
</div>
- 上記の例では子から親要素にイベントが伝搬しているように見えますが、実際には下図のように3つのフェーズを通してイベントが伝播しています。
Graphical representation of an event dispatched in a DOM tree using the DOM event flow
1. キャプチャーフェーズ (Capture Phase)
- イベントが発生した際、DOMツリーの一番外の要素(
window
)から、イベントが発生した要素(td
)の親要素(tr
)までイベントが伝搬します。 - 実際には3.バブリングフェーズでイベント発生に対する処理を実行するため、ほとんど使うケースはありません。
- このフェーズでイベントの発生に対して処理を実行したい場合、イベントリスナーにて以下の通り実装すれば可能です。
el.addEventListener(someFunction, { capture: true })
// または
el.addEventListener(someFunction, true)
2. ターゲットフェーズ (Target Phase)
- ターゲット要素の親要素(
tr
)からターゲット要素(td
)にイベントが伝搬するフェーズです。 - このフェーズではハンドラは呼び出されませんが、1.キャプチャフェーズと 3.バブリングフェーズ の両方のハンドラがこのフェーズで呼び出されます。
3. バブリングフェーズ (Bubbling Phase)
-
ターゲット要素(
td
)から、DOMツリーの一番外の要素(window
)までイベントが伝播します。 - 登録されたイベントリスナーやイベントハンドラーは基本このフェーズで呼び出されます。
- 基本的に全てのイベントはバブリングしますが、
focus
やblur
など一部イベントは例外的にバブリングしません。
Reactのイベントシステム
1. 処理の登録方法
- JSXの
onClick
やonChange
などの特定のイベントプロパティに対して、イベントハンドラーを渡す形で登録します。
<button onClick={someFunction} />
- JSXのプロパティに渡されたイベントハンドラーは、内部でReactツリー上のアプリがマウントされるルート要素に対してイベントリスナーとして登録されています。
- これは、イベントの移譲(Event Delegation) と呼ばれる手法で、この内部的な実装によりパフォーマンスが最適化されています。
イベントの移譲
共通の親要素に対してひとつのイベントリスナーもしくはハンドラを設定することで、子要素で発生するイベントを一つの親要素で検知して処理するイベントのハンドリングパターンです。
<ul onclick="(e)=> console.log(e.target.id)">
<li id="1">A</li>
<li id="2">B</li>
<li id="3">C</li>
</ul>
2. イベントオブジェクト
- Reactのイベントオブジェクトはブラウザのイベントオブジェクトと違い 合成イベント(SyntheticEvent) と呼ばれます。
- 合成イベントは、各ブラウザのネイティブイベントをラップしたイベントオブジェクトで、ブラウザ間のイベントの違いを吸収して動作が同じになるように実装されています。
- 以下は主なコンポーネントがサポートする合成イベントの一覧です。
3. イベントの伝搬
- DOMツリーではなくReactツリーの構造に従って伝搬します。
例えばcreatePortal
を使って実装されたportal要素は、DOMツリーとReactツリーで位置が違いますが、Reactツリー内での親子関係に従ってイベントが伝搬します。 - イベントに対する処理は常にバブリングフェーズで実行されます。
-
scroll
イベントを除き、基本的に全てのイベントがバブリングします。
例えば、合成イベントのfocus
やblur
は、ブラウザのイベントと違い、内部的にfocusin
やfocusout
イベントに置き換えられて実装されているためバブリングします。 - キャプチャーフェーズでイベントに対する処理を実行したい場合、
on〇〇Capture
というプロパティにハンドラを登録れば可能です。
// クリックイベント伝搬のキャプチャフェーズに実行
<button onClickCapture={someFunction}>クリック</button>
まとめ
Reactのイベントシステムはブラウザのイベントシステムと比較し、使いやすく最適化されていることが理解できました。
参考
Discussion