Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

追記

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