Node.js イベントループ

イベントループは Node.js が非ブロッキング I/O 操作を処理する中核メカニズムであり、シングルスレッドで複数の同時リクエストを効率的に処理できるようにします。

Node.js はシングルスレッドベースの JavaScript ランタイムであり、イベントループを利用してファイル読み取り、ネットワークリクエスト、データベースクエリなどの非同期操作を処理します。

イベントループにより、Node.js はコードをノンブロッキングで実行し、複数の接続を処理し、非同期 I/O 操作を実行できます。

イベントループにより、Node.js はスレッドをブロックすることなく大量の同時 I/O 操作を処理できます。これは Node.js が同時リクエストを効率的に処理するための鍵です。

イベントループの段階

イベントループは複数の段階に分かれており、各段階が特定のタスクを処理します。主要な段階は次のとおりです:

  • Timers:実行setTimeout()とsetInterval()のコールバック。
  • I/O Callbacks:遅延された I/O コールバックを処理する。
  • Idle, prepare:内部で使用され、あまり一般的ではない。
  • Poll:新しい I/O イベントを取得し、I/O 関連のコールバックを実行する。
  • Check:実行setImmediate()コールバック。
  • Close Callbacks:クローズコールバックを処理します。例えばsocket.on('close', ...)。

イベントループの流れ

  • タスクがイベントループキューに入る。
  • イベントループは段階の順序に沿って処理を行い、各段階には独自のコールバックキューがあります。
  • イベントループはpoll段階で新しいイベントの到着を待ち、イベントがない場合は他の段階のコールバックを確認します。
  • もしsetImmediate()とsetTimeout()が両方存在する場合、setImmediate()にcheck段階が先に実行され、setTimeout()にtimers段階が実行されます。

例

setTimeout(() => {
  console.log('Timeout callback');
}, 0);

setImmediate(() => {
  console.log('Immediate callback');
});

console.log('Main thread execution');

出力順序:

  • Main thread execution先に出力されます。
  • setImmediate()とsetTimeout()の実行順序は現在のイベントループの状態に依存し、一般的にsetImmediate()が先に実行されます。

マクロタスクとマイクロタスク

  • マクロタスク:setTimeout、setInterval、setImmediate、I/O 操作など。
  • マイクロタスク:process.nextTick、Promise.then。

実行順序:マイクロタスクはマクロタスクよりも優先度が高く、現在の段階のコールバック終了後にすぐに実行されます。

例

setTimeout(() => {
  console.log('Timeout callback');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise callback');
});

console.log('Main thread execution');

実行出力結果:

Main thread execution
Promise callback
Timeout callback

process.nextTick()

process.nextTick() は現在の操作が終了した後、次の段階が始まる前にマイクロタスクを実行します。優先度は Promise より高くなります。

例

process.nextTick(() => {
  console.log('Next tick callback');
});

console.log('Main thread execution');

出力:

Main thread execution
Next tick callback

イベント駆動型プログラム

Node.js では、イベント駆動型プログラミングは主に EventEmitter クラスによって実装されます。

EventEmitter は events モジュール内の組み込みクラスです。EventEmitter を継承することで、独自のイベントエミッタを作成し、イベントの登録とトリガーを行うことができます。

このメカニズムにより、Node.js は非同期タスクを効率的に処理でき、シングルスレッド環境でも並行処理を実現できます。

イベント駆動の一連の流れはこのように実装されており、非常に簡潔です。これはオブザーバーパターンに似ています。イベントはテーマ(Subject)に相当し、このイベントに登録されたすべての処理関数はオブザーバー(Observer)に相当します。

基本概念:

  • イベント:プログラム内で発生するアクションまたは状態の変化。例えば、ファイルの読み取り完了や HTTP リクエストの到着など。
  • イベントトリガー:EventEmitterNode.js の組み込みモジュールであり、イベントの発行と監視に使用されます。
  • イベントハンドラー:イベントに関連付けられたコールバック関数で、イベント発生時に呼び出されます。

イベント駆動の流れ:

  • イベントの登録:プログラム内でEventEmitterインスタンスを使用してイベントと対応するハンドラーを登録します。
  • イベントのトリガー:指定されたイベントが発生したとき、EventEmitterそのイベントがトリガーされます。
  • イベントの処理:イベントループは対応するコールバック関数をスケジュールしてタスクを実行します。

Node.js には複数の組み込みイベントがあります。events モジュールをインポートし、EventEmitter クラスをインスタンス化することで、イベントをバインドおよび監視できます。次の例のとおりです:

// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();

以下のプログラムはイベントハンドラーをバインドします:

// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);

プログラムを通じてイベントをトリガーできます:

// 触发事件
eventEmitter.emit('eventName');

例

hello.js ファイルを作成し、コードは次のとおりです:

例1

const EventEmitter = require('events');
const myEmitter = new EventEmitter();

// イベントハンドラーを登録
myEmitter.on('greet', () => {
  console.log('Hello, world!');
});

// イベントをトリガー
myEmitter.emit('greet');

出力:

Hello, world!

main.js ファイルを作成し、コードは次のとおりです:

例2

//events モジュールをインポート var events = require('events'); //eventEmitter オブジェクトを作成 var eventEmitter = new events.EventEmitter(); //イベントハンドラーを作成 var connectHandler = function connected() { console.log('接続成功。'); //data_received イベントをトリガー eventEmitter.emit('data_received'); } //connection イベントハンドラーをバインド eventEmitter.on('connection', connectHandler); //匿名関数を使用して data_received イベントをバインド eventEmitter.on('data_received', function(){ console.log('データ受信成功。'); }); //connection イベントをトリガー eventEmitter.emit('connection'); console.log("プログラムの実行が完了しました。");

次に、上記のコードを実行してみましょう:

$ node main.js
连接成功。
数据接收成功。
程序执行完毕。

Node アプリケーションはどのように動作するのですか?

Node アプリケーションでは、非同期操作を実行する関数はコールバック関数を最後のパラメータとして受け取ります。コールバック関数は最初のパラメータとしてエラーオブジェクトを受け取ります。

次に、前述の例を改めて見てみましょう。input.txt を作成し、ファイルの内容は次のとおりです:

サンプル官网地址:www.example.com

main.js ファイルを作成し、コードは次のとおりです:

var fs = require("fs");

fs.readFile('input.txt', function (err, data) {
   if (err){
      console.log(err.stack);
      return;
   }
   console.log(data.toString());
});
console.log("程序执行完毕");

上記のプログラムでは、fs.readFile() はファイルを読み取るための非同期関数です。ファイルの読み取り中にエラーが発生すると、エラーオブジェクト err がエラーメッセージを出力します。

エラーが発生しなかった場合、readFile は err オブジェクトの出力をスキップし、ファイルの内容はコールバック関数を通じて出力されます。

上記のコードを実行すると、実行結果は次のとおりです:

程序执行完毕
サンプル官网地址:www.example.com

次に input.txt ファイルを削除すると、実行結果は次のとおりです:

程序执行完毕
Error: ENOENT, open 'input.txt'

input.txt ファイルが存在しないため、エラーメッセージが出力されました。

その他の拡張