45
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Organization

イベントを聞くPromiseのメモリリークとその対策

追記

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は賢く元のリスナを見て削除してくれる。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
45
Help us understand the problem. What are the problem?