雑なRxJS入門

2017/8/25
RxJSを活用した
Angularアプリの状態管理設計

hashtag #d3_angular

自己紹介

  • 西田雅博
  • 株式会社ビズリーチ HRMOS事業部 エンジニア
  • 最近はフロントエンドメイン、サーバーサイドもやってました
  • Angular4, Play Framework (Scala), React
avatar

雑?

この勉強会の後半で必要になる
RxJSの前提知識だけカバーします

もくじ

  • RxJSってなに
  • Observable
  • Subject
  • BehaviorSubject

RxJSってなに

非同期やイベントのコールバックをmap/filter/reduceなどのコレクションのAPIぽく扱えるライブラリ
            Rx.Observable.fromEvent(window, 'click')
  .distinctUntilChanged(
    (prev, next) => prev.pageX === next.pageX
  )
  .filter(ev => ev.pageX < 400 && ev.pageY < 400)
  .map(ev => 'mouse click, X:' + ev.pageX + ', Y:' + ev.pageY)
  .subscribe(msg => console.log(msg));
          

このスライドに出てくるコードをCopyボタンでコピーしてコンソールに貼って動かしてみてください

JSのふつうのイベント・非同期ハンドリング

                          
var button = document.querySelector('#the-button');
button.addEventListener('click', () => console.log('Clicked!'));

Promise.resolve('oi')
  .then(msg => console.log(msg));
            
          

RxJSだとこう

                          
var button2 = document.querySelector('#the-button2');
Rx.Observable.fromEvent(button2, 'click')
  .subscribe(() => console.log('Clicked!'));

Rx.Observable.fromPromise(Promise.resolve('oi'))
  .subscribe(msg => console.log(msg));
            
          

RxJSだとなにがうれしいのか?

  • イベント・非同期の扱いが統一される
  • map/filterなど便利オペレータが山盛り
    • 自分で書いたらだるすぎる・バグりまくるような処理もイケる

うれしくなくてもAngularが
使ってるので覚えざるを得ない😂

RxJSは難しい?

  • たぶんYES
  • オペレータが100個以上あったりする
  • そもそも非同期・イベントハンドリングが難しい?

公式ドキュメントが勉強になる

RxJS(の難しさ)を分解する

6つのコンセプトに分類できる

  • Observable
  • Observer
  • Subscription
  • Operators
  • Subject
  • Schedulers
  • Observable, Observer
    • データの発生、消費
    • 一番基本的な概念
  • Subject
    • データのマルチキャスト
    • 今回の主役
  • Operators
    • データの加工
    • 今回はmap/switchMapくらい
  • Subscription, Schedulers
    • 今回は出番なし

Observable

  • 値を発生するもの
  • イベント、非同期など
  • Observableを生成する静的メソッドが多数ある
  • from, range, fromEventなど
            
Rx.Observable.from(['foo', 'bar', 'baz', 'qux'])
  .subscribe(console.log); // 'foo', 'bar', 'baz', 'qux'
Rx.Observable.range(1,4)
  .subscribe(console.log); // 1,2,3,4
Rx.Observable.fromEvent(window, 'mousemove')
  .subscribe(console.log); // すごいことに
            
          

Observable

  • next, error, completeの3種類の値を発生させる
  • nextは0〜無限の数だけ発生する
  • error, completeのどちらかが発生するとそれ以降値は発生しない
            myObservable
  .subscribe({
    next: (x) => console.log(x),
    error: (e) => console.error(e),
    complete: () => console.log('completed')
  });
// nextだけの場合短く書ける
// myObservable.subscribe((x) => console.log(x))
          

Observer

  • 前のページにも出てきたnext, error, completeのコールバックを持つオブジェクト
                          
var observer = {
  next: (x) => console.log(x),
  error: (e) => console.error(e),
  complete: () => console.log('completed')
};
            
          

Observable.create

  • 一番プリミティブなObservable生成メソッド
  • constructorのエイリアス
  • 引数にObserverを取り、nextなどで操作

XMLHttpRequestと組み合わせてオレオレRxJSHttpクライアントが作れそう?

            var observable = Rx.Observable.create((observer) => {
  observer.next(1);
  observer.next(2);
  setTimeout(() => {
    observer.next(3);
    observer.complete();
  }, 1000);
});
observable.subscribe(
  (x) => console.log('value: ' + x),
  (e) => console.log(e),
  () => console.log('completed')
);
          

Observable百景

Angularの車窓から

AngularのHttpとComponent

              @Component({
  template: `
    <h1>User</h1>
    <p>{{ user.name }}<p>`
})
export class UserComponent {
  user: User;
  constructor(private http: HttpClient) {
    http.get<User>('/api/user')
      .subscribe(user => this.user = user);
  }
}
            

async パイプ版

  • asyncパイプを使うとsubscribe/unsubscribeを
    よしなにやってくれる
  • ngIfやngForとの併用が便利
              @Component({
  template: `
    <div *ngIf="user$ | async as user else loading">
      <p>{{ user.name }}</p>
    </div>
    <ng-template #loading>
      <span>loading</span>
    </ng-template>`
})
export class UserComponent {
  // Observable<T>な変数に$をつける文化っぽい
  user$: Observable<User>
  constructor(private http: HttpClient) {
    this.user$ = http.get<User>('/api/user');
  }
}
            

Httpの4xx/5xxレスポンス

              // true:ログイン成功 false:失敗
login(name: string, password: string): Observable<boolean> {
  return http.post('/login', { name, password })
    .mapTo(true)
    .catch((res: HttpErrorResponse) => {
      if (res.status === 401) {
        return Observable.of(false);
      } else {
        return Observable.throw(res);
      }
    });
}
            

Router

  • パス、クエリパラメータ等の変更はObservableになっている
            
constructor(
  private route: ActivatedRoute,
  private service: HeroService
) {}

ngOnInit() {
  // /heroes?id=10 のようなクエリパラメータの更新を監視
  this.route.paramMap
    .switchMap((params: ParamMap) =>
      this.service.getHero(params.get('id'))
    )
    .subscribe((hero: Hero) => this.hero = hero);
}
            
          

サジェスト表示

              @Component({
  template: `
    <input type="text" #input
           (keyup)="onChange(input.value)" />
    <p>さては{{ suggest$ | async }}だなオメー</p>`
})
export class AppComponent {
  word$ = new Subject<string>();
  suggest$: Observable<string>

  constructor(private http: HttpClient) {
    this.suggest$ = this.word$
      .debounceTime(500)
      .distinctUntilChanged()
      .switchMap(word => {
        return this.http.get<string>(`/search?word=${word}`);
      });
  }

  onChange(word: string) {
    this.word$.next(word);
  }
}
            

Observableの性質

  • Observableは一つのObserverしか受け付けない
  • Observable.createからもそんな感じがする
    • subscribeしたら中の処理が実行されるだけ
            // 再掲
var observable = Rx.Observable.create((observer) => {
  observer.next(1);
  observer.next(2);
  setTimeout(() => {
    observer.next(3);
    observer.complete();
  }, 1000);
});
observable.subscribe(
  (x) => console.log('value: ' + x),
  (e) => console.log(e),
  () => console.log('completed')
);
          

Observableの性質

  • Observableの値を色んな所で使いたい場合困る
            
// Angularのコード
getUser(): Observable<User> {
  // http.get<T>はObservable<T>を返す
  return this.http.get<User>('/api/user');
}
// ユーザーデータをUserProfileComponentでも
// UserAvatarComponentでも使いたい
// けど2回API叩くのは良くない
// getUser()を実行するたびにHTTPリクエストが飛ぶ
            
          

Subject

  • 値をマルチキャストできるObservable
  • next, error, completeメソッドが生えたObservable
  • Subjectのnextなどのメソッドを実行するとSubjectをsubscribeしているObserverに値が配信される
            // user$をいろんなところからsubscribeする
public user$ = new Subject();
getUser() {
  this.http.get<User>('/api/user')
    .subscribe(user => user$.next(user));
    // user$をsubscribeしている全員に値が配信される
}
          

BehaviorSubject

  • Subjectの派生クラス
  • Subject+現在値
  • subscribeした瞬間に現在値を返してくれる

下記コードをコンソール上に貼り付けて実行した後、どちらかのボタンを押してsubscribeする。
BehaviorSubjectはその時点の値が流れてくるが、Subjectは次のsetInterval実行まで(最大2秒間)値が来ない

            window.behaviorSubject = new Rx.BehaviorSubject(0);
window.subject = new Rx.Subject();
window.count = 0;
setInterval(() => {
  behaviorSubject.next(++count);
  subject.next(count);
}, 2000);
          

まとめ

  • RxJSってなに
    • 非同期・イベントをまとめて抽象化したAPI群
  • いくつかのコンセプトに分けられる
  • 今回触れたのはObservable, Observer, Subject
  • ObservableはObserverと1対1なので値を複数の場所に流せない
  • Subjectは値をマルチキャストできる
  • Subjectの派生クラスにBehaviorSubjectがあり、subscribeするとその時点での値を流してくれる