イベント駆動型プログラミングは、複雑さと衝突の問題を回避するためにプログラミングを制限することを選択できる論理パターンです。 この記事では、イベント駆動型プログラミングがどのように機能するか、そしてNode.jsプロジェクトでそれを最大限に活用する方法について説明します。

ほとんどの開発者は、プログラミングの研究の早い段階でイベント駆動型プログラミングの概念を紹介されていますが、少し後になるまで完全には理解できない可能性があります。 概念はかなり遍在していることがわかります。 そこにある主要なフレームワークまたはソフトウェアを確認してください。イベント駆動型プログラミングの証拠が見つかる可能性があります。

概要

あらゆるレベルのプログラミングスキルを持つ人々のためのイベント駆動型プログラミングの最もよく知られている例については、私たちの旧友であるWebブラウザに目を向けます。

ユーザーインターフェイスを介してWebページを操作するたびに、イベントが発生します。 ボタンをクリックすると、クリックイベントがトリガーされます。 キーを押すと、keydownイベントがトリガーされます。 これらのイベントには関連する関数があり、トリガーされると、何らかの方法でユーザーインターフェイスに変更を加えるために実行されます。

イベント駆動型プログラミングは、次の概念を利用します。

  • イベントハンドラーは、イベントがトリガーされたときに呼び出されるコールバック関数です。
  • メインループはイベントトリガーをリッスンし、そのイベントに関連付けられたイベントハンドラーを呼び出します。

EventEmitter

Node.jsは、 EventEmitter と呼ばれる便利なモジュールをネイティブに提供します。これにより、プロジェクトにイベント駆動型プログラミングをすぐに組み込むことができます。 もちろん、独自のバージョンのEventEmitterを作成することはそれほど難しいことではありません。実際、npmには、EventEmitter2EventEmitter3など、より高速なパフォーマンスを約束するいくつかのモジュールが公開されています。ネイティブのEventEmitterよりも。

これらは両方とも、プロジェクトをEventEmitterが許可するよりも速く実行する必要があるかどうかを確認する価値があります。 これらは両方とも、EventEmitterに使用する構文とほぼ同じ構文を可能にするように構築されているため、1つを学習すると、すべての構文を簡単に操作できるようになります。

eventsモジュールを介してEventEmitterクラスにアクセスします。 インポートしたら、クラスから新しいオブジェクトを作成して使用を開始する必要があります。

const EventEmitter = require('events').EventEmitter;
const myEventEmitter = new EventEmitter;

これで、Nodeでのイベント駆動型プログラミングを開始できます。

チャットルームを作成していると想像してください。 新しいユーザーがチャットルームに参加したときに、すべての人に警告したいと思います。 userJoinedイベントのイベントリスナーが必要です。 まず、イベントリスナーとして機能する関数を記述し、次にEventEmittersonメソッドを使用してリスナーを設定できます。

const EventEmitter = require('events').EventEmitter;
const chatRoomEvents = new EventEmitter;

function userJoined(username){
  // Assuming we already have a function to alert all users.
  alertAllUsers('User ' + username + ' has joined the chat.');
}

// Run the userJoined function when a 'userJoined' event is triggered.
chatRoomEvents.on('userJoined', userJoined);

次のステップは、誰かがログインするたびにチャットルームがuserJoinedイベントをトリガーして、イベントハンドラーが呼び出されるようにすることです。 EventEmitterには、イベントをトリガーするために使用するemitメソッドがあります。 チャットルームモジュール内のログイン関数内からこのイベントをトリガーしたいと思います。

function login(username){
  chatRoomEvents.emit('userJoined', username);
}

ユーザーがログアウトしたとき、メッセージが送信されたとき、メッセージが受信されたとき、またはチャットルームを必要なだけ動的にするために必要となる可能性のあるその他のイベントを作成することで、さらに拡張できます。

リスナーの削除

イベントリスナーをイベントから削除したいときが来るでしょう。 これは、パフォーマンス上の理由(イベントが不要になった)またはメモリリークを回避するため(イベントリスナーが不要になったオブジェクトを参照した場合、ガベージコレクションできなくなります)である可能性があります。 これにより、不要なオブジェクトが蓄積する可能性があります)。

EventEmitterでイベントリスナーを削除するには、removeListenerまたはremoveAllListenersメソッドを使用できます。 Nodeに組み込まれているEventEmitterでは、removeListenerメソッドを使用するときに削除する正確な関数への参照を渡す必要があることに注意してください。 つまり、イベントを削除する場合は常に、コード内のその場所から関数を参照できることを確認する必要があります。 このため、イベントリスナーを登録する前に、匿名のままにするのではなく、イベント処理関数に名前を付けて宣言するのが最適な場合がよくあります。

次の例では、 message イベントのリスナーを、クロージャ内で宣言された無名関数であるため、userJoined関数の外部から削除するのは困難です。 この場合、このメソッドを直接参照できるのは、EventEmitterオブジェクト自体だけです。 これは、単一のイベントに複数のリスナーが登録されている場合、どのリスナーが目的のターゲットであるかを解読する方法を見つけ出す必要があるため、実用的ではありません。

const EventEmitter = require('events').EventEmitter;
const chatRoomEvents = new EventEmitter;

function userJoined(username){
  chatRoomEvents.on('message', function(message){
    document.write(message);
  })
}

chatRoomEvents.on('userJoined', userJoined);

次のようにコードを書き直すと、このような頭痛の種をすべて回避できます。

const EventEmitter = require('events').EventEmitter;
const chatRoomEvents = new EventEmitter;

function displayMessage(message){
  document.write(message);
}

function userJoined(username){
  chatRoomEvents.on('message', displayMessage);
}

chatRoomEvents.on('userJoined', userJoined);

ここで、 messageイベントのハンドラーのリストからdisplayMessage関数を削除する場合:

chatRoomEvents.removeListener('message', displayMessage);

オブジェクト指向プログラミング+イベント駆動型プログラミング

ここで最後に触れたいのは、オブジェクト指向プログラミングパラダイムとイベント駆動型プログラミングパラダイムの組み合わせです。 これら2つは、さまざまな状況で非常に価値のある組み合わせになります。その理由を理解して概念化することは有益であると思います。

オブジェクト指向アプローチは、個々のユニット(またはオブジェクト)のすべての動作がそのユニット内のコードから処理されるという考えを促進します。 このアプローチを使用して、アプリケーションは、すべてが相互に通信および相互作用する多くの異なるユニットで構築されます。

メールアプリケーションを構築していると想像してください。 クライアントの受信メールメッセージと送信メールメッセージを処理することを唯一の目的とするオブジェクトがある場合があります。 このオブジェクトには、独自の動作関数がすべて含まれます。 メールをサーバーに配信するsendMail関数があるかもしれません。 また、receiveMail関数を使用して、サーバーに新しいメールを配信するように指示することもできます。 これらのサーバーの相互作用を担当するオブジェクトをMailboxと呼びます。

const Mailbox = {
  sendMail: function(){
    // code to send mail.
  },
  receiveMail: function(){
    // check server for new mail.
  }
}

これで、sendMail関数を利用したい他のユニットを構築している場合、それらはMailbox.sendMailを介してアクセスできます。 これは標準的なアプローチです。 しかし、後でメールを送信するたびにログに記録することを決定した場合はどうなりますか? ここで、 sendMail 関数を変更してこの動作を組み込むか、sendMail関数の直後にトリガーされる別の関数を作成する必要があります。 この場合、 sendMail 関数のトリガーを担当するオブジェクトは、必ずログ関数もトリガーする必要があります。 アプリケーションがより複雑になり、新しい動作シーケンスが追加されると、これはやや手に負えなくなる可能性があります。

ここで、イベント駆動型プログラミングを利用できます。 イベントリスナーを登録することで、オブジェクト間の通信の流れを実際に逆にすることができます。 オブジェクトが関数をトリガーするために内部に到達する必要があるのではなく、オブジェクトはイベントを発行するだけで、それらのイベントをリッスンしているオブジェクトは、指示された方法でそれを処理します。 オブジェクトの動作のソースは、外部オブジェクトからアクセスする必要がなく、完全にそれ自体の中に含まれるようになりました。

ガトー級潜水艦に代表される空腹のワニがいると想像してみましょう。 イベント駆動型ではないアプローチを使用すると、ワニの食事のプロセスは次のようになります。


class Food {
  constructor(name) {
    this.name = name;
  }

  becomeEaten() {
    return 'I have been eaten.';
  }
}

var bacon = new Food('bacon');

class gator {
  eat() {
    bacon.becomeEaten();
  }
}

この例では、ゲーターは食べるためにFood内のメソッドにアクセスする必要がありました。 これは私たちの怠惰なゲイターにとっては大変な作業なので、彼のために物事を簡単にするつもりです。

const EventEmitter = require('events').EventEmitter;
const myGatorEvents = new EventEmitter;

class Food {
  constructor(name) {
    this.name = name;
    // Become eaten when gator emits 'gatorEat'
    myGatorEvents.on('gatorEat', this.becomeEaten);
  }

  becomeEaten(){
    return 'I have been eaten.';
  }
}

var bacon = new Food('bacon');

const gator = {
  eat() {
    myGatorEvents.emit('gatorEat');
  }
}

これで、gatorが行う必要があるのは、 gatorEat と言うだけで、残りはEventEmitterが処理します。