RxJSの勉強
公式
学習サイト
クリックイベントを取得する
rxjsを使わないパターン
const appEl = document.getElementById("app")!;
appEl.addEventListener("click", () => {
console.log("clickイベント発火");
});
rxjsを使ったパターン
import { fromEvent } from "rxjs";
const appEl = document.getElementById("app")!;
const clickObservable$ = fromEvent(appEl, "click");
clickObservable$.subscribe(() => console.log("clickイベント発火"));
Observableとは?
streamで流れてきたデータを、データ加工や間引き処理などの処理を行う部分。
e.g. 水道(stream)に水(データ・イベント)を流し、蛇口(Observable)でろ過処理(フィルタリング)を行う。
Observableは、next
・error
・completed
の3つの処理を持つ。
Observableの例
// 1,2,3,(1秒待機),4,(完了)
const observable$ = new Observable(s => {
s.next(1);
s.next(2);
s.next(3);
setTimeout(() => {
s.next(4);
s.complete();
}, 1000);
});
observable$.subscribe(console.log);
// appEl.addEventListener("click", console.log);
const observable$ = fromEvent(appEl, "click");
observable$.subscribe(console.log);
// window.addEventListener("resize", console.log);
const observable$ = fromEvent(window, "resize");
observable$.subscribe(console.log);
// let count = 0;
// setInterval(() => {
// console.log(count);
// count++;
// }, 1000);
const observable$ = interval(1000);
observable$.subscribe(console.log);
pipeについて
Observableから新たなObservableを作成する。pipeの引数には、複数のオペレータを指定できる。
例: windowのリサイズを検知
const onResizeWindow$: Observable<Event> = fromEvent(window, "resize");
onResizeWindow$.subscribe((event: Event) => console.log(event));
上記のEventを流すObservableから、画面サイズを返すObservableを作成する。
type WindowSize = {
width: number;
height: number;
};
const onResizeWindowSize$ = onResizeWindow$.pipe((obs) => {
return new Observable<WindowSize>((s) => {
obs.subscribe((event) => {
s.next({
width: window.innerWidth,
height: window.innerHeight,
});
});
});
});
onResizeWindowWidth$.subscribe((size: WindowSize) => console.log(size));
map
というオペレータを使うと、もっと簡単に書ける。
const onResizeWindowSize$ = onResizeWindow$.pipe<WindowSize>(
map((event) => {
return {
width: window.innerWidth,
height: window.innerHeight
};
})
);
throttle と debounce について
throttle
とは、連続して大量に繰り返される処理を一定間隔で間引くこと。
debounce
とは、連続して大量に繰り返される処理が指定時間内に何度発生しても、最後の一回だけを実行すること。
windowのリサイズの例
windowのリサイズは、連続して大量に繰り返されるので、throttle
やdebounce
を用いて、いい感じに調整する。
デバッグ用にロギング関数を用意
const logger = (data: any) => {
const now = new Date();
const timeLabel = `${now.getSeconds()}.${now.getMilliseconds()}s`;
console.log(timeLabel, data);
};
素の処理
const onResizeWindowSize$ = onResizeWindow$.pipe<WindowSize>(
map((event) => {
return {
width: window.innerWidth,
height: window.innerHeight
};
})
);
onResizeWindowSize$.subscribe(logger);
画面サイズが変化している途中でも、ログが出力される。
throttle(500ms)
const onResizeWindowSizeWithThrottle$ = onResizeWindowSize$.pipe(
throttleTime(500)
);
onResizeWindowSizeWithThrottle$.subscribe(logger);
画面サイズが変化し続けても、500ms間隔でしかログが出力されない。
debounce(500ms)
const onResizeWindowSizeWithDebounce$ = onResizeWindowSize$.pipe(
debounceTime(500)
);
onResizeWindowSizeWithDebounce$.subscribe(logger);
500ms間、画面サイズが変化しない状態が続くと、今の画面サイズがログとして出力される。
ResizeObserverで画面サイズを監視
resizeイベントは、windowでのみ提供されているため、特定のelementのサイズを監視するためには、ResizeObserver
を使用する必要がある。
// windowを監視
window.addEventListener("resize", () => {
const size = {
width: window.innerWidth,
height: window.innerHeight
};
console.log(size);
});
// id="app"のelementを監視
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
const size = { width, height };
console.log(size);
});
const appEl = document.getElementById("app")!;
observer.observe(appEl);
上記の処理をRxJSに書き換える
const createResizeObservable = (el: Element) => {
return new Observable<WindowSize>((s) => {
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
s.next({ width, height });
});
observer.observe(el);
}).pipe(debounceTime(500));
};
const appEl = document.getElementById("app")!;
const onResizeAppSize$ = createResizeObservable(appEl);
onResizeAppSize$.subscribe(console.log);
avaでRxJSのテストを書く
テスト対象の関数を実装する
何らかのデータ・イベントが流れたときに、まずtrue
を返し、x秒後にfalse
を返すObservableを生成する関数createToggleFlagObservable
を実装する
import { delay, map, merge, Observable } from "rxjs";
export const createToggleFlagObservable = (
onEvent$: Observable<unknown>,
delayTime: number = 1000
) => {
const enableFlag$ = onEvent$.pipe(map(() => true));
const disableFlag$ = onEvent$.pipe(delay(delayTime)).pipe(map(() => false));
const toggleFlag$ = merge(enableFlag$, disableFlag$);
return toggleFlag$;
};
ユースケース
特定のエリア(element)をクリックしたときに、ポップアップを表示し、5秒後にポップアップを非表示にする。
const appEl = document.getElementById("app")!;
const clickAppEl$ = fromEvent(appEl, "click");
const toggleFlag$ = createToggleFlagObservable(clickAppEl$, 5000);
toggleFlag$.subscribe((flag) => {
if (flag) {
console.log("ポップアップを表示");
} else {
console.log("ポップアップを非表示");
}
});
テストを実装
import { TestScheduler } from "rxjs/testing";
import test from "ava";
test("クリックした後にポップアップを表示し、2秒後にポップアップを非表示", (t) => {
const scheduler = new TestScheduler(t.deepEqual.bind(t));
scheduler.run(({ hot, expectObservable }) => {
const onEvent$ = hot("a", { a: undefined });
const toggleFlag$ = createToggleFlagObservable({
onEvent$,
delayTime: 2000
});
expectObservable(toggleFlag$).toBe("a 1999ms b", {
a: true,
b: false
});
});
});