12周年記念パーティ開催! 2024/5/10(金) 19:00

RxJS入門 第1回 RxJSとは

RxJSは、リアクティブプログラミングという考え方のもとに構築されているライブラリです。「いつ起こるかわからない処理」の扱いを、一貫性を持って書くことができます。 まずはコード例で、その感触をつかんでみてください。

発行

著者 奥野 賢太郎 フロントエンド・エンジニア
RxJS入門 シリーズの記事一覧

はじめに

今シリーズではRxJSを紹介します。RxJSとは、端的にいうとJavaScriptでのイベントのハンドリングや非同期処理をラップし、扱いやすくするためのライブラリです。

このライブラリは、もともと2009年にマイクロソフト社によってC#用に開発されたRx.NETというライブラリのJavaScript向けの移植で、他にもRxの名を冠するライブラリがさまざまな言語向けに移植されています。そのため、RxJSを学ぶと他の言語向けのRx利用者とも同じ土俵で会話ができるという、おもしろい背景を持っています。

いつ起こるかわからない処理

JavaScriptでの開発では「いつ起こるかわからない処理」というものが頻繁に登場します。RxJSはこういった処理をラップし、RxJSを使うことで処理の書き方に一貫性を持たせるためのライブラリです。この記事では、RxJSがどのように「いつ起こるかわからない処理」をラップして書き方を統一できるのか紹介したいと思います。

そこでまず、この「いつ起こるかわからない処理」について、我々はどのようなコードを書いてきたのか、振り返ってみましょう。

イベントハンドリング

JavaScriptでは、往々にしてイベントをハンドリングしなければならない場面がでてきます。クリックされたときの処理、スクロールしたら値を変更、ウインドウのリサイズに応じて位置を調整、などなど。これらのイベントは、ユーザーがいつその操作をするかわかりません。つまり、いつ起こるかわからない処理といえます。

いつ起こるかわからない処理に対しては、JavaScriptにおいては関数を引数に指定することで、そのときの処理内容を記述します。この引数に指定しているものはFunction Objects(関数オブジェクト)で、俗にコールバック、リスナー、ハンドラーなどの名前で呼ばれているものです。

スクロール位置をログに出力するイベントハンドリングの例

window.addEventListener('scroll', () => {
  console.log(window.pageYOffset)
})

簡単な処理であれば、まだ処理の全容を把握することもできます。しかし、ここにひとたび何十行もの手続きが書かれてしまうと、その処理は追いにくいものとなってしまいます。

たとえば、通常のクリック、Altキーを押しながらのクリック、シフトキーを押しながらのクリックに応じて、それぞれ処理を分けて書きたいとしましょう。このとき、まず思いつくのはif文による分岐です。

const buttonEl = document.querySelector('#click-me')
const resultEl = document.querySelector('#result')

buttonEl.addEventListener('click', ev => {
  const spanEl = document.createElement('span')
  if (ev.altKey) {
    spanEl.innerText = 'altKey!'
  } else if (ev.shiftKey) {
    spanEl.innerText = 'shiftKey!'
  } else {
    spanEl.innerText = 'clicked!'
  }
  resultEl.appendChild(spanEl)
})

このように、リスナー関数の引数evを扱い、プロパティaltKeyshiftKeyの状態をif文内で検証し分岐する例です。

このようにソースコードの上から処理を書き、途中で分岐したり繰り返したりするプログラミング手法を手続き型と呼びます。手続き型では、求める動作そのものの構造化がされておらず、上から順に書かれることが起こりやすいです。その結果、長い行数のコードは読みにくく、変更しにくいものとなっていきます。

コードが長くなったとき、しばしば処理を別の関数に分割するリファクタリングを行うことがあります。ただ、分割をしたり、そもそも長い処理を書かないように気を付けるかどうかは、指針がなければ、人それぞれとなってしまいます。

昨今のWebアプリケーションではクリック、スクロール、ドラッグ、ドロップ、どの操作をとっても高度な処理を求められることが増えました。そのため、保守しやすい開発を心がけようとすると、イベントハンドリングの処理は、肥大化に注意しながら組み立てていくことが求められるようになります。

非同期通信処理

もうひとつ、JavaScriptで「いつ起こるかわからない処理」の代表格といえば、非同期処理です。

JavaScriptはシングルスレッドで動作する言語ですが、それゆえに非同期処理を多用する文化となりました。XHRを使ったHTTP通信ではonreadystatechangeにイベントハンドラーを登録することで、通信中に続きの処理をブロックせずに先に進めて、通信完了の状態を受け取ったときにハンドラーに登録した処理を実行することができます。このHTTP通信をラップして、より使いやすくしたものがjQueryの$.ajax()でした。

のちに、JavaScriptには言語レベルでPromiseが実装されるようになりました。Promiseは現代JavaScriptにおける非同期を扱う形式として、もはやおなじみになり、XHRに続くHTTP通信用のAPIとして策定されたfetch()も、このPromiseを返すようになります。

ただ、単一のPromiseは通信状態を表しているオブジェクトではなく、あくまでも「戻ってくることが約束された値」です。何らかの待ちが発生する処理(たとえばHTTP通信)に対して、そこから戻ってくる値を表しているにすぎず、複数の通信結果を組み合わせた処理を書く場合に、Promiseでは煩雑になりがちです。

たとえば、複数のAPIの結果を使わなければならない場面を考えてみましょう。AというAPIのレスポンスに含まれる値を、BというAPIのリクエストボディに含まなければならない……。Promiseの.then()の中で再度別のAPIを呼び、また.then()を書くということになり、読みづらくなってしまいます。

ネストが深まるthenの例

fetch('some/api/a')
  .then(res => {
    const id = res.id
    fetch('some/api/b', { method: 'POST', body: {id} })
      .then(res => {
        // BのAPIの完了処理
      })
  })

Promiseの.then()コールバックでは、この関数の戻り値としてPromiseを返すこともできます。そしてPromiseを返したとき、.then()を続けて書くことができます。そのため、次のようにも書けます。

連なるthenの例

fetch('some/api/a')
  .then(res => {
    const id = res.id
    return fetch('some/api/b', { method: 'POST', body: {id} })
  })
  .then(res => {
    // BのAPIの完了処理
  })

コールバックのネストが深くならず、少しシンプルになりました。

ただし、このアプローチでは求めた結果は得られたとしても、まだ問題が残ります。この例では、複数のPromiseを合わせただけで、コードの書き方にも幅が生まれています。書き方に幅があるということは、指針を決めない限りチームで開発する上で、さまざまな書き方ができてしまう可能性があるということです。これではアプリケーションを通しての一貫性が失われてしまいがちです。

複数人のエンジニアで開発していくとき、アプリケーションの規模が大きくなるにつれて、徐々に一貫性のズレは保守しにくいものとなります。これは、コードを読む際の引っ掛かりにも繋がっていきます。

シンプルに、そして一貫性を

JavaScript開発において、頻繁に対応しなければならない「いつ起こるかわからない処理」は、処理を関数オブジェクトとしてまとまった単位で書かなければならない性質上、人によって書き方がぶれてしまうことがありえます。if文で分岐が繰り返される長大な処理、複数のAPIを呼ぶための.then()チェーンの連なり。こんな処理を何か一貫性を持って捌けないでしょうか?

それを達成できるライブラリがRxJSです。

リアクティブプログラミング

ライブラリの名前にもあるRxとは略称で、省略しなければReactive Extensionsという言葉になります。では、ここにでてくる「リアクティブ」とはどういう意味でしょうか。

あるひとつのプログラミングの考え方を表す言葉として「プログラミングパラダイム」というものがあります。たとえば「オブジェクト指向プログラミング」はパラダイムのひとつで、他にもさまざまなパラダイムがあります。

その中のひとつ「リアクティブプログラミング」というパラダイムが、今回紹介するものです。リアクティブプログラミングはしばしばRPとも書かれます。

RPとは、時間とともに変化する値に対する操作を宣言的に記述していくプログラミングパラダイムです。時間とともに変化する値とは、たとえばタイマー、マウスカーソルやスクロールの位置、HTTP経由で送られてくるデータなどです。つまり、これまでに説明した「いつ起こるかわからない処理」の全般を指します。そして、これら時間とともに変化する値のひとまとまりを、ストリーム(stream)と呼んで扱います。

【ワンポイント】ストリーム

JavaScriptにおいてストリームといえば、Node.jsにはfs.createReadStreamというものがあります。これはファイルサイズの大きいファイルを読み込む際に、データをチャンクという細かく分割した単位で徐々に読み込み、最終的にすべてのデータが揃うというAPIです。

この徐々に読み込むという点は、まさに「時間とともに変化する値」です。Node.js上で述べられるストリームは、ここにRxJSこそ登場していませんが、同じ概念です。

ストリームは流れや小川を意味する英単語であり、そのためしばしばストリームの解説では川に例えられることもあります。

宣言的に記述する

ストリームに対して操作を宣言的に記述するとは、どういうことか説明します。

buttonEl.addEventListener('click', ev => {
  const spanEl = document.createElement('span')
  if (ev.altKey) {
    spanEl.innerText = 'altKey!'
  } else if (ev.shiftKey) {
    spanEl.innerText = 'shiftKey!'
  } else {
    spanEl.innerText = 'clicked!'
  }
  resultEl.appendChild(spanEl)
})

これは前節で紹介したイベントハンドリングの例です。今後if文が増えていったり、中の処理が多くなると見通しが悪くなる懸念がある、というものでした。これをストリームとして扱うとどうなるでしょうか。

const buttonClick$ = Rx.Observable.fromEvent(buttonEl, 'click')

buttonClick$
  .filter(ev => !ev.altKey && !ev.shiftKey)
  .subscribe(ev => {
    const spanEl = document.createElement('span')
    spanEl.innerText = 'clicked!'
    resultEl.appendChild(spanEl)
  })

buttonClick$
  .filter(ev => ev.altKey)
  .subscribe(ev => {
    const spanEl = document.createElement('span')
    spanEl.innerText = 'altKey!'
    resultEl.appendChild(spanEl)
  })

buttonClick$
  .filter(ev => ev.shiftKey)
  .subscribe(ev => {
    const spanEl = document.createElement('span')
    spanEl.innerText = 'shiftKey!'
    resultEl.appendChild(spanEl)
  })

クリックイベントをストリームとして扱うために登場するのがRxJSです。ストリームを作成するために、ここではRx.Observable.fromEvent()というAPIを使っています*。ここでは、JavaScriptのDOMイベントをストリームに変換するためのものと考えてください。

*注:RxJSの利用方法

RxJSは、CDN経由で読み込んだときRxというグローバル変数を持ちます。npm経由で読み込む際には、動的にグローバル変数が増やされることはありません。RxJSを読み込んで使うための手順は次回説明していきます。

一見するとif文よりさらに長くなってしまったように見えます。そこで、.subscribe()に注目してみましょう。.subscribe()もRxJSのAPIのひとつです。時間とともに変化する値がどうなっているか観測するためのAPIで、addEventListener()のリスナー関数に相当します。

ボタンがクリックされたら、.subscribe()の中の関数が作動します。ここまではaddEventListener()とよく似ています。つづいて、その前に書かれた.filter()を見てみましょう。.filter()を書くことで、ストリームはそのフィルタがtrueを返すときにしか先に流れません。こうすることで、addEventListener()との大きな違いとして.subscribe()にはif文がひとつも出てきません。

if文のないコードはよいものです。なぜなら関数の引数に値が入力されてから、戻り値として出力されるまでの処理は、常に一定となるからです。常に一定であるということは引数を与えるだけで戻り値が確定し、デバッグがとてもやりやすいものとなります。

今回の例では、条件分岐させるための条件を.filter()内に、行いたい処理そのものを.subscribe()に分けて別々の関数にしたことで、それぞれの関数内ではif文が登場せず、関数単位のデバッグが容易になりました。

このように、特定の条件下で結果が一定となるように、条件と処理を宣言的に書いていく。そして複数の小さく単純な関数を組み合わせることで、達成したい処理を作っていく。このプログラミング手法がリアクティブプログラミングです。

PromiseをRxJSで扱う

addEventListenerの代わりにRxJSでイベントを同様に扱えるようにするAPIがObservable.fromEvent()でした。もうひとつ「いつ起こるかわからない処理」として紹介したPromiseについても、RxJSで扱えます。それがObservable.fromPromise()です。

// Promiseを返す処理
const fetchPromise = fetch('path/to')

// fromPromise()でストリーム化
const response$ = Rx.Observable.fromPromise(fetchPromise)

Promiseをストリームとして扱うためにはfromPromise()にPromiseを渡すだけなので、とても簡単です。そのストリームに流れる値の処理には、.then()の代わりに.subscribe()を使います。

ここでは.subscribe().filter()などを詳しく説明せずに、いきなり紹介しますが、詳しいAPIの解説は次回以降に行います。まずはRxJSがイベントやPromiseを統一的に扱えて、そのときのコードはこのような見た目になる、と覚えておいてください。

まとめ

JavaScriptには「いつ起こるかわからない処理」が多く、それらに対処するため「リアクティブプログラミング(RP)」というパラダイムを活用して宣言的に記述していくためのライブラリがRxJSである、ということがおわかりいただけたかと思います。

ここで注意しておきたいのは、RPを実践するためにRxJSが必須なわけではありません。あくまでも、このパラダイムを実践しやすくする「指針」をRxJSがライブラリとして提供しているにすぎません。そのため、RxJSを使わなくてもRPはできるという意見や、Promiseの書き方を注意すればRxJSがなくても十分に書けるという意見があることも理解できます。

RxJSは、ライブラリとしてRPの手法をまとまった単位にしたことで、このライブラリを採用するプロジェクト全体にRPを浸透させやすくしているものだと、筆者は感じています。

次回は実際に使うためのインストールの方法や、例をまじえながら基礎的なAPIを紹介しつつ、実際にアプリケーションではどのようにRxJSを組み込んで活用できるかを説明します。

奥野 賢太郎
フロントエンド・エンジニア

音楽業界に飛び込み、音楽制作・ディレクター・DTM科講師などをつとめる。同時期にWebデザインのバイトをきっかけにAngularJSと出会う。その後、フロントエンド・エンジニアへの転身を決心し、自社サービスを運営する企業に入社。設計や実装に携わる。2017年1月に株式会社ピクセルグリッドへ入社。得意分野はAngularを用いたWebアプリケーションの構築。音楽知識を基にしたWeb Audio、Web MIDIの技術にも明るい。2018年、退社。

この記事についてのご意見・ご感想 この記事をXにポストする

全記事アクセス+月4回配信、月額880円(税込)

CodeGridを購読する

初めてお申し込みの方には、30日間無料でお使いいただけます