Observable触らんかね?

@pastelInc

Observable ⊆ Angular2

Contents

  • 即結果を反映するフォームの実装
  • Observableの提案

即結果を反映するフォームの実装

タイピング中にページを検索する機能

screen-google

どうやって実装しよう

function search(event) {
  const xhr = new XMLHttpRequest();
  const query = event.target.value;
  const url = location.href;

  xhr.onreadystatechange = function(){
    if (this.readyState === 4 && this.status === 200) {
      render(this.responseText);
    }
  };
  xhr.open('GET', url, true);
  xhr.send();
};
function listen(element, eventName) {
  element.addEventListener(eventName, search, true);
}

const inputElement = document.querySelector('input');

listen(inputElement, 'keyup');

リクエストの数を減らそう

Timer導入!

Timerを使ったメンタルモデル

ユーザーのキータイピング

キータイピング停止

タイマーが止まっていたらタイマースタート

タイマーが0になったらリクエスト

タイマーが動いていたらタイマーストップ/リセット

class Timer {
  private id;
  
  constructor() {
    this.id = null;
  }
  
  get isStarted() {
    return this.id !== null;
  }
  
  start(cb, msec) {
    this.id = setTimeout(cb, msec);
  }
  
  stop() {
    clearInterval(this.id);
  }
  
  reset() {
    this.id = null;
  }
}
function search(event) {
  const xhr = new XMLHttpRequest();
  const query = event.target.value;
  const url = location.href;

  xhr.onreadystatechange = function(){
    if (this.readyState === 4 && this.status === 200) {
      render(this.responseText);
    }
  };
  xhr.open('GET', url, true);
  if (!timer.isStarted) {
    timer.start(xhr.send, 500);
  } else {
    timer.stop();
    timer.reset();
  }
}

同じクエリパラメータを無視しよう

Cache導入!

Cacheを使ったメンタルモデル

ユーザーのキータイピング

キータイピング停止

タイマーが止まっていたらタイマースタート

タイマーが0になったらCache評価開始

Cacheと同じクエリパラメータならリクエストしない

Cacheと異なるクエリパラメータならリクエスト

タイマーが動いていたらタイマーストップ/リセット

class CacheQuery {
  private query;
  
  constructor() {
    this.query = '';
  }
  
  get myQuery() {
    return this.query;
  }
  
  save(query) {
    this.query = query;
  }
}
function search(event) {
  const xhr = new XMLHttpRequest();
  const query = event.target.value;
  const url = location.href;

  xhr.onreadystatechange = function(){
    if (xhr.readyState === 4 && xhr.status === 200) {
      render(xhr.responseText);
    }
  };
  xhr.open('GET', url, true);
  if (!timer.isStarted) {
    timer.start(() => {
      if (cache.myQuery !== query) {
        xhr.send();
        cache.save(query);
      }
    });
  } else {
    timer.stop();
    timer.reset();
  }
}

後からリクエストした結果が必ず表示されるようにしよう

大変!難しい!

状態の共有があればいけそう

リクエストとレスポンスをペアとするような状態を何処かで実現しなければいけない。

{
  "id": 1 // リクエストのBodyにリクエストIDを設定する。
}

今までのメンタルモデルを振り返る

現実世界の時間変化 = 計算機内での時間変化として考えていました。

これでわかった問題

複数プロセス(オブジェクト)の同期が必要

先程の例ではサーバと状態の同期(共有)が必要になりました。
他にもオブジェクトが互いに状態のやり取りを行う必要がありました。

事象の順序の制約

私達が直感的に感じている順番で事象が起こるとは限らない。

いい方法はないでしょうか

Observableを使いましょう!!!

Observableの提案

状態を保持した可変オブジェクトが不要

Observableを実装すると先程の例のクラスが必要なくなります。

listen

function listen(element, eventName) {
    return Rx.Observable.create(observer => {
        let handler = event => observer.next(event);

        element.addEventListener(eventName, handler, true);

        return _ => {
            element.removeEventListener(eventName, handler, true);
        };
    });
}

search

function search(event) {
  const xhr = new XMLHttpRequest();
  const query = event.target.value;
  const url = location.href;
  const observable = Rx.Observable.create(observer => {
    xhr.onreadystatechange = function(){
      if (xhr.readyState === 4 && xhr.status === 200) {
        observer.next(xhr);
      }
    };
  });

  xhr.open('GET', url, true);
  xhr.send();
  return observable;
}

Observable生成

const inputElement = document.querySelector('input');
const observable = listen(inputElement, 'keyup')
  .debounceTime(500) // 500msの間、keyupがなければデータソースを発信
  .distinctUntilChanged() // 重複したデータソースをフィルタ
  .switchMap((event) => search(event)) // レスポンスの前後が逆になった場合、後だけを発信するようにフィルタ
;

observable.subscribe({
  next: xhr => {
    render(xhr.responseText);
  }
});

これがObservableの力!

Observableの概念

DOMイベント、timer intervals、socketのようなプッシュ型データソースをモデル化するために使います。

subscribe

Observableでモデル化したプッシュ型のデータソースの評価を実行します。
この評価を後処理可能なオブジェクトを返します。

subscribeするまで変化は起きない

http://jsbin.com/soboyo/edit?js,output

map/filter

Observableにはmap/filterといった馴染みのメソッドが実装されるかもしれません。

map/filterはEcma262の提案に入ってないから注意!

keyupイベントをリストのように扱う

https://jsbin.com/bipozi/2/edit?js,console,output

Observableで簡単!

Observableによって、プッシュ型データソースを使った手続きを簡単に書くことができます。

Implementations

Observableはまだ提案中なので使いたい時には以下のライブラリを利用できます。

  • RxJS 5 (beta)
  • zen-observable

Observable ⊆ RxJS

RxJSはObservableを提供してくれます。RxJSを使うとmap/filter以外にもいろんなOperatorを使うことができます。

Angular2で結果を即反映するフォームの実装

export class App {
  items: Observable<Array<string>>;
  term = new Control();
  constructor(private wikipediaService: WikipediaService) {
    // subscribeしていないことがポイント
    this.items = this.term.valueChanges
                 .debounceTime(500)
                 .distinctUntilChanged()
                 .switchMap(term => this.wikipediaService.search(term));
  }
}

AsyncPipeでObservableを扱える

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Wikipedia Search</h2>
      <input type="text" [ngFormControl]="term"/>
      <ul>
        <li *ngFor="#item of items | async">{{item}}</li>
      </ul>
    </div>
  `
})

さぁ、Observableを触らんかね?

学習リソース

ありがとうございました!

@pastelInc