57
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-02-03

追記

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

57
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?