イベント駆動型のプログラムをきれいに作る
JavaScriptのプログラムは、イベント駆動型で非同期な構造が基本となります。他のプログラミング言語で一般的な「同期型」の関数では、例えばネットワーク経由でデータを読み込む場合、データを読み込む関数を実行するとその読み込みが完了するまでプログラムの実行は止まったままになります。
それに対して、「非同期型」の関数を使うとデータの読み込みが完了するまで他の処理を実行でき、読み込みが完了した後で特定の関数を呼び出してもらうことができます。身近な例に例えると、メールを送信して相手からの返信が来るまで何もせずに待つのが同期型関数だとすれば、非同期型関数は返信を待つ間に他の仕事をこなすことができます。
Webアプリケーションはネットワーク通信部分に最も時間がかかる場合が多いため、その待ち時間に他の処理をこなせば、ただ待っているよりも遥かに効率よく多くの処理をこなすことができます。Node.jsの標準APIは入出力に関する多くの関数が非同期化されており、効率良く処理を行うプログラムを簡単に書けるようになっています。ただし非同期型関数を多用するとプログラムの流れが複雑になり、ソースコードが読みづらくなるという欠点もあります。
fs = require 'fs' data = fs.readFileSync '/etc/hosts', 'utf8' console.log data
fs = require 'fs' fs.readFile '/etc/hosts', 'utf8', (err, data) -> #(2) throw err if err console.log data #(1)
リスト1のプログラムは従来通り上から下へと処理が進み、fs.readFileSync()で/etc/hostsというファイルの読み込みが完了するまでプログラムは次の行には進みません。一方、リスト2のプログラムでは「読み込みが完了した時に実行する関数」をfs.readFile()の第3引数に指定しています。このように後から実行してもらうよう引数として渡す関数のことをコールバック関数と呼びます。リスト2では、/etc/hostsファイルの読み込み中もプログラム本筋の処理は止まらずに先に(1)の部分が実行され、ファイルの読み込みが完了した後で(2)の部分が実行されます。
リスト2でfs.readFile()を呼んでから(2)の部分がどのくらい後に実行されるかはプログラムが実際に実行されるまでわかりません。この例のように単純なプログラムならばよいですが、複雑なプログラムになるにつれ、コールバック関数だけですべての非同期処理を行うのは大変な作業になっていきます。そこで解決策の1つとして登場するのがEventEmitterです。
EventEmitter
Node.jsにはEventEmitterというイベント通知用のクラスが標準で用意されています。EventEmitterを使うと、ソースコード中でお互い離れた箇所にあるコールバック関数を実行できます。その際、イベント名を手がかりとして実行対象のコールバック関数を見つけます。ここでのイベント名とは、例えて言うとメールアドレスのようなものと考えてください。あらかじめ特定のイベント名、つまりメールアドレスを作っておき、それを相手に知らせることで相手はメールアドレスを手がかりにメッセージを送れます。EventEmitterはNode.jsにおける郵便システムのようなもので、多くのサードパーティのライブラリもEventEmitterを使っています。
EventEmitterはリスト3のように使うことができます。
{EventEmitter} = require 'events' # 上の行は EventEmitter = require('events').EventEmitter と同等 emitter = new EventEmitter emitter.on 'arrive', (where) -> console.log "#{where}に到着しました" emitter.emit 'arrive', '家'
家に到着しました
ここでは、まずemitter.on(イベント名, コールバック関数)を呼ぶことでコールバック関数をイベントリスナとして登録しています。イベントリスナとは、特定のイベントが起きた時の通知先のことです。そしてemitter.emit(イベント名, 引数1, 引数2, ...)でイベントを発行すると、イベントリスナであるコールバック関数が実行されます。emit()の第2引数以降はコールバック関数にそのまま渡されます。
1つのファイル内だけでEventEmitterを使う分にはこのような使い方でも構いませんが、大規模なプログラムや外部に公開するライブラリを作るときはEventEmitterを継承したクラスを作るとよいでしょう。EventEmitterを継承したクラスはEventEmitterのメソッドをすべて持ちます。
{EventEmitter} = require 'events' # EventEmitterを継承したMyTimerクラスを定義する class MyTimer extends EventEmitter trigger: -> # alarmイベントを発行する this.emit 'alarm', 'MyTimer' # MyTimerクラスのインスタンスを作る timer = new MyTimer # alarmイベントが発行された時に実行するコールバック関数を登録 timer.on 'alarm', (origin) -> console.log "alarm received from #{origin}" timer.trigger()
alarm received from MyTimer
リスト4のようにクラスを作ったら、MyTimerクラスを外部からrequire()経由で使えるようにしておきましょう。EventEmitterは標準APIのため上記のような使い方が広く認知されており、他人から見ても使用方法がわかりやすいという利点があります。
EventEmitterの主なメソッド
emitter.addListener(event, listener) emitter.on(event, listener)
eventが発行された時に関数listenerを実行するようイベントリスナとして登録します。onという関数名でも使うことができます。
emitter.once(event, listener)
eventが発行された時に1度だけ実行される関数listenerを登録します。2度目以降のeventには反応しません。
emitter.emit(event, [arg1], [arg2], [...])
イベントを発行します。eventでイベント名を指定します。そのイベントについて登録されたリスナがすべて実行されます。第2引数以降(任意)が指定された場合は、イベントリスナに引数としてそのまま渡されます。
emitter.removeListener(event, listener)
登録したイベントリスナを削除します。
emitter.removeAllListeners([event])
すべてのイベントリスナを削除します。event(任意)が指定された場合は、そのイベントについて登録されたリスナだけを削除します。
EventEmitter2
EventEmitterの機能を拡張したEventEmitter2(MITライセンス)というサードパーティのライブラリがあります。EventEmitterの機能に加え、イベント名をドット区切りのネームスペースでフィルタリングする機能などを備えています(リスト5)。
{EventEmitter2} = require 'eventemitter2' # ワイルドカードによるフィルタリングを有効にしたインスタンスを作成 reporter = new EventEmitter2 wildcard:true # イベント名の先頭が「東京.」の時に実行 reporter.on "東京.*", (value) -> console.log "【東京】#{value}" # イベント名の先頭が「大阪.」の時に実行 reporter.on "大阪.*", (value) -> console.log "【大阪】#{value}" # イベント名が「東京.天気」の時に実行 reporter.on "東京.天気", (value) -> console.log "【東京の天気】#{value}" # イベント名の末尾が「.天気」の時に実行 reporter.on "*.天気", -> console.log "天気情報" # イベント名の末尾が「.気温」の時に実行 reporter.on "*.気温", -> console.log "気温情報" # すべてのイベントに対して実行 reporter.onAny (value) -> console.log "更新あり" console.log "[1]" reporter.emit "東京.気温", "気温 11度" console.log "[2]" reporter.emit "大阪.気温", "気温 14度" console.log "[3]" reporter.emit "東京.天気", "天気 曇り" console.log "[4]" reporter.emit "大阪.天気", "天気 晴れ"
[1] 更新あり 【東京】気温 11度 気温情報 [2] 更新あり 【大阪】気温 14度 気温情報 [3] 更新あり 【東京の天気】天気 曇り 【東京】天気 曇り 天気情報 [4] 更新あり 【大阪】天気 晴れ 天気情報
詳しい使い方は、公式ドキュメントを参考にしてください。