追記
Node v11.13.0にて、 events
モジュールに once
が追加された。可能ならこちらを使うと面倒がない。本記事で説明しているリスナの解除までやってくれる。
ref. https://nodejs.org/api/events.html#events_events_once_emitter_name
const { once, EventEmitter } = require('events');
const ee = new EventEmitter();
for(let i = 0; i < 20; ++i) {
once(ee, 'done').then(() => console.log('done'));
ee.emit('done');
}
ES2015でのPromiseの標準化や昨今のFluxブームに伴って、Promiseでイベントを聞く場面は増加しているものと思う。
メモリリークの例
const EventEmitter = require('events').EventEmitter;
const ee = new EventEmitter();
for(let i = 0; i < 20; ++i){
new Promise((resolve, reject)=>{
ee.once('error', reject);
ee.once('done', resolve);
});
ee.emit('done');
}
たとえばこんなコードを書いたとする。.once
でリスナを登録して満足してしまっているが、一度も'error'
が発行されないので、'error'
の方に登録したリスナは登録されたままになる。
実際、上記のコードをreplから実行してみると、
(node) warning: possible EventEmitter memory leak detected. 11 error listeners added. Use emitter.setMaxListeners() to increase limit.
Trace
at EventEmitter.addListener (events.js:239:17)
at EventEmitter.once (events.js:265:8)
at repl:3:4
at repl:2:1
at REPLServer.defaultEval (repl.js:164:27)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.<anonymous> (repl.js:393:12)
at emitOne (events.js:82:20)
at REPLServer.emit (events.js:169:7)
こんな出力が得られる。EventEmitterは、イベント名に対して11個以上登録された状態になるとこのような警告を発する。この個数は.setMaxListeners
から変更が可能である。
解決法
リスナが呼ばれてPromiseの状態が確定するときに、もう一方のリスナも解除してやればよい。
const EventEmitter = require('events').EventEmitter;
const ee = new EventEmitter();
for(let i = 0; i < 20; ++i){
new Promise((resolve, reject)=>{
function onDone(arg){
resolve(arg);
cleanUp();
}
function onError(arg){
reject(arg);
cleanUp();
}
function cleanUp(){
ee.removeListener('done', onDone);
ee.removeListener('error', onError);
}
ee.on('error', onError);
ee.on('done', onDone);
});
ee.emit('done');
}
ここで、.on
ではなく.once
を使用してもよい。.once
は内部的に上記コードのようなラッパをリスナとして登録しているが、.removeListener
は賢く元のリスナを見て削除してくれる。