fromEvent() - イベントをObservableに変換
fromEvent()は、DOMイベントやNode.js EventEmitterなどのイベントソースを、Observableストリームに変換するCreation Functionです。
概要
fromEvent()は、イベントベースの非同期処理をRxJSのパイプラインで扱えるようにします。購読時に自動的にイベントリスナーを登録し、購読解除時には自動的にリスナーを削除するため、メモリリークのリスクを大幅に軽減できます。
シグネチャ:
function fromEvent<T>(
target: any,
eventName: string,
options?: EventListenerOptions | ((...args: any[]) => T)
): Observable<T>公式ドキュメント: 📘 RxJS公式: fromEvent()
基本的な使い方
DOMイベントをObservableとして扱う最もシンプルな例です。
import { fromEvent } from 'rxjs';
const clicks$ = fromEvent(document, 'click');
clicks$.subscribe(event => {
console.log('ボタンがクリックされました:', event);
});
// クリックするたびにイベントが発行される重要な特徴
1. 自動的なリスナー登録・解除
fromEvent()は、購読時にイベントリスナーを登録し、購読解除時に自動的にリスナーを削除します。
import { fromEvent } from 'rxjs';
const clicks$ = fromEvent<MouseEvent>(document, 'click');
const subscription = clicks$.subscribe(event => {
console.log('クリック位置:', event.clientX, event.clientY);
});
// 5秒後に購読解除(イベントリスナーも自動削除)
setTimeout(() => {
subscription.unsubscribe();
console.log('購読解除しました');
}, 5000);IMPORTANT
メモリリーク防止
unsubscribe()を呼ぶと、内部でremoveEventListener()が自動的に実行されます。これにより、手動でリスナーを削除する必要がなく、メモリリークのリスクが大幅に軽減されます。
2. Cold Observable(各購読が独立したリスナーを登録)
fromEvent()によって作成されるObservableはCold Observableです。購読するたびに、独立したイベントリスナーが登録されます。
import { fromEvent } from 'rxjs';
const clicks$ = fromEvent(document, 'click');
// 購読1 - リスナーAを登録
clicks$.subscribe(() => console.log('Observer 1: クリック'));
// 1秒後に購読2を追加 - リスナーBを独立して登録
setTimeout(() => {
clicks$.subscribe(() => console.log('Observer 2: クリック'));
}, 1000);
// 1回のクリックで両方のリスナーが発火
// これは各購読が独立したリスナーを持つ証拠NOTE
Cold Observableの証明
購読するたびに新しいイベントリスナーが登録され、購読解除時に削除されます。これはCold Observableの特徴です。ただし、イベントソース(DOM要素など)は外部にあり共有されるため、「購読前のイベントは受け取れない」というHot的な性質も持ちます。
3. TypeScriptの型サポート
イベントの型を明示的に指定できます。
import { fromEvent } from 'rxjs';
const input = document.createElement('input');
input.type = 'text';
document.body.appendChild(input);
const input$ = fromEvent<InputEvent>(input, 'input');
input$.subscribe(event => {
// eventの型はInputEvent
const target = event.target as HTMLInputElement;
console.log('入力値:', target.value);
});4. Cold Observable
fromEvent()はCold Observableです。購読するたびに独立した実行が開始されます。
import { fromEvent } from 'rxjs';
const button = document.createElement('button');
button.innerText = "購読";
document.body.appendChild(button);
const clicks$ = fromEvent(document, 'click');
// 1回目の購読 - イベントリスナーが追加される
clicks$.subscribe(() => console.log('購読者A'));
// 2回目の購読 - 別のイベントリスナーが追加される
clicks$.subscribe(() => console.log('購読者B'));
// 1回クリックすると両方のリスナーが発火
// 出力:
// 購読者A
// 購読者BNOTE
Cold Observableの特徴
- 購読するたびに独立した実行が開始されます
- 各購読者は独自のデータストリームを受け取ります
- 購読ごとに独立したイベントリスナーが登録されます。unsubscribeで自動的にリスナーが解除されます。
詳しくは コールドObservableとホットObservable を参照してください。
実践的なユースケース
1. クリックイベントの処理
ボタンクリックを制御し、連続クリックを防止します。
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs';
const button = document.createElement('button');
button.innerText = "submit";
document.body.appendChild(button);
const clicks$ = fromEvent(button, 'click');
clicks$.pipe(
debounceTime(300), // 300ms以内の連続クリックを無視
map(() => '送信中...')
).subscribe(message => {
console.log(message);
// API呼び出しなどの処理
});2. フォーム入力のリアルタイム検証
入力イベントをストリーム化し、リアルタイムでバリデーションを実行します。
import { fromEvent } from 'rxjs';
import { map, debounceTime, distinctUntilChanged } from 'rxjs';
const label = document.createElement('label');
label.innerText = 'email: ';
const emailInput = document.createElement('input');
label.appendChild(emailInput);
document.body.appendChild(label);
const email$ = fromEvent<InputEvent>(emailInput, 'input');
email$.pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(500), // 入力が止まってから500ms後に処理
distinctUntilChanged() // 値が変わった時のみ
).subscribe(email => {
console.log('検証対象:', email);
// メールアドレスのバリデーション処理
validateEmail(email);
});
function validateEmail(email: string): void {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
console.log(isValid ? '有効なメールアドレス' : '無効なメールアドレス');
}3. ドラッグ&ドロップの実装
マウスイベントを組み合わせて、ドラッグ&ドロップを実装します。
import { fromEvent } from 'rxjs';
import { switchMap, takeUntil, map } from 'rxjs';
// ドラッグ可能な要素を作成
const element = document.createElement('div');
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = '#333';
element.style.position = 'absolute'; // 絶対配置に設定
element.style.left = '50px'; // 初期位置
element.style.top = '50px';
element.style.cursor = 'move'; // ドラッグ可能なカーソル
document.body.appendChild(element);
const mousedown$ = fromEvent<MouseEvent>(element, 'mousedown');
const mousemove$ = fromEvent<MouseEvent>(document, 'mousemove');
const mouseup$ = fromEvent<MouseEvent>(document, 'mouseup');
mousedown$.pipe(
switchMap(startEvent => {
// 要素内のクリック位置を記録
const startX = startEvent.clientX - element.offsetLeft;
const startY = startEvent.clientY - element.offsetTop;
return mousemove$.pipe(
map(moveEvent => ({
left: moveEvent.clientX - startX,
top: moveEvent.clientY - startY
})),
takeUntil(mouseup$) // マウスアップで終了
);
})
).subscribe(({ left, top }) => {
// 要素の位置を更新
element.style.left = `${left}px`;
element.style.top = `${top}px`;
});4. スクロールイベントの監視
無限スクロールやスクロール位置の追跡に使用します。
import { fromEvent } from 'rxjs';
import { throttleTime, map } from 'rxjs';
const scroll$ = fromEvent(window, 'scroll');
scroll$.pipe(
throttleTime(200), // 200msごとに1回のみ処理
map(() => window.scrollY)
).subscribe(scrollPosition => {
console.log('スクロール位置:', scrollPosition);
// ページ下部に到達したら追加コンテンツを読み込む
if (scrollPosition + window.innerHeight >= document.body.scrollHeight - 100) {
console.log('追加コンテンツを読み込み');
// loadMoreContent();
}
});パイプラインでの使用
fromEvent()は、イベントストリームを起点としたパイプライン処理に最適です。
import { fromEvent } from 'rxjs';
import { map, filter, scan } from 'rxjs';
const button = document.createElement('button');
button.innerText = "カウンター";
document.body.appendChild(button);
const clicks$ = fromEvent(button, 'click');
clicks$.pipe(
filter((event: Event) => {
// Shiftキーを押しながらのクリックのみカウント
return (event as MouseEvent).shiftKey;
}),
scan((count, _) => count + 1, 0),
map(count => `クリック回数: ${count}`)
).subscribe(message => console.log(message));よくある間違い
1. 購読解除を忘れる
❌ 間違い - 購読解除を忘れるとメモリリークの原因に
import { fromEvent } from 'rxjs';
function setupEventListener() {
const clicks$ = fromEvent(document, 'click');
clicks$.subscribe(console.log); // 購読解除されない!
}
setupEventListener();✅ 正しい - 必ず購読解除する
import { fromEvent } from 'rxjs';
import { Subscription } from 'rxjs';
let subscription: Subscription;
function setupEventListener() {
const clicks$ = fromEvent(document, 'click');
subscription = clicks$.subscribe(console.log);
}
function cleanup() {
if (subscription) {
subscription.unsubscribe();
}
}
setupEventListener();
// コンポーネント破棄時などにcleanup()を呼ぶWARNING
メモリリークに注意
SPAやコンポーネントベースのフレームワークでは、コンポーネント破棄時に必ず購読解除してください。購読解除を忘れると、イベントリスナーが残り続けてメモリリークの原因になります。
2. 複数のイベントリスナーを重複登録
❌ 間違い - 同じイベントに複数回購読すると、複数のリスナーが登録される
import { fromEvent } from 'rxjs';
const clicks$ = fromEvent(document, 'click');
clicks$.subscribe(() => console.log('Observer 1'));
clicks$.subscribe(() => console.log('Observer 2'));
// クリックすると両方のログが表示される(2つのリスナーが登録されている)✅ 正しい - 必要に応じてshare()でマルチキャスト
import { fromEvent } from 'rxjs';
import { share } from 'rxjs';
const clicks$ = fromEvent(document, 'click').pipe(share());
clicks$.subscribe(() => console.log('Observer 1'));
clicks$.subscribe(() => console.log('Observer 2'));
// 1つのリスナーが共有されるパフォーマンスの考慮事項
高頻度で発火するイベント(scroll, mousemove, resize等)を扱う際は、パフォーマンスに注意が必要です。
TIP
高頻度イベントの最適化:
throttleTime()- 一定時間ごとに1回のみ処理debounceTime()- 入力が止まってから処理distinctUntilChanged()- 値が変わった時のみ処理
❌ パフォーマンス問題 - リサイズのたびに処理
import { fromEvent } from 'rxjs';
const resize$ = fromEvent(window, 'resize');
resize$.subscribe(() => {
console.log('リサイズ処理'); // 高負荷処理
});✅ 最適化 - 200msごとに1回のみ処理
import { fromEvent } from 'rxjs';
import { throttleTime } from 'rxjs';
const resize$ = fromEvent(window, 'resize');
resize$.pipe(
throttleTime(200)
).subscribe(() => {
console.log('リサイズ処理'); // 負荷軽減
});関連するCreation Functions
| Function | 違い | 使い分け |
|---|---|---|
| from() | 配列・Promiseから変換 | イベント以外のデータをストリーム化 |
| interval() | 一定間隔で発行 | 定期的な処理が必要 |
| fromEventPattern() | カスタムイベント登録 | EventEmitter以外の独自イベントシステム |
まとめ
fromEvent()はDOMイベントやEventEmitterをObservableに変換- 購読時にリスナー登録、購読解除時に自動削除(メモリリーク防止)
- Hot Observableとして動作
- 必ず購読解除を実行してメモリリークを防ぐ
- 高頻度イベントは
throttleTime()やdebounceTime()で最適化