Skip to content

Commit 7061669

Browse files
mscdexbnoordhuis
authored andcommitted
events: optimize adding and removing of listeners
These optimizations result in >2x speedup in the ee-add-remove benchmark: * Don't mutate array.length when removing the last listener for an event * Don't bother checking max listeners if listeners isn't an array * Don't call delete when removing the last event in _events, just re-assign a new object instead PR-URL: #785 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Evan Lucas <[email protected]>
1 parent 630f636 commit 7061669

File tree

1 file changed

+69
-45
lines changed

1 file changed

+69
-45
lines changed

lib/events.js

+69-45
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ EventEmitter.init = function() {
3030
}
3131
}
3232

33-
if (!this._events || this._events === Object.getPrototypeOf(this)._events)
33+
if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
3434
this._events = {};
35+
this._eventsCount = 0;
36+
}
3537

3638
this._maxListeners = this._maxListeners || undefined;
3739
};
@@ -115,15 +117,18 @@ function emitMany(handler, isFn, self, args) {
115117
EventEmitter.prototype.emit = function emit(type) {
116118
var er, handler, len, args, i, events, domain;
117119
var needDomainExit = false;
120+
var doError = (type === 'error');
118121

119122
events = this._events;
120-
if (!events)
121-
events = this._events = {};
123+
if (events)
124+
doError = (doError && events.error == null);
125+
else if (!doError)
126+
return false;
122127

123128
domain = this.domain;
124129

125130
// If there is no 'error' event listener then throw.
126-
if (type === 'error' && !events.error) {
131+
if (doError) {
127132
er = arguments[1];
128133
if (domain) {
129134
if (!er)
@@ -189,39 +194,47 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
189194
throw new TypeError('listener must be a function');
190195

191196
events = this._events;
192-
if (!events)
197+
if (!events) {
193198
events = this._events = {};
194-
else {
199+
this._eventsCount = 0;
200+
} else {
195201
// To avoid recursion in the case that type === "newListener"! Before
196202
// adding it to the listeners, first emit "newListener".
197203
if (events.newListener) {
198204
this.emit('newListener', type,
199-
typeof listener.listener === 'function' ?
200-
listener.listener : listener);
205+
listener.listener ? listener.listener : listener);
206+
207+
// Re-assign `events` because a newListener handler could have caused the
208+
// this._events to be assigned to a new object
209+
events = this._events;
201210
}
202211
existing = events[type];
203212
}
204213

205-
if (!existing)
214+
if (!existing) {
206215
// Optimize the case of one listener. Don't need the extra array object.
207216
existing = events[type] = listener;
208-
else if (typeof existing !== 'function')
209-
// If we've already got an array, just append.
210-
existing.push(listener);
211-
else
212-
// Adding the second element, need to change to array.
213-
existing = events[type] = [existing, listener];
214-
215-
// Check for listener leak
216-
if (typeof existing !== 'function' && !existing.warned) {
217-
m = $getMaxListeners(this);
218-
if (m && m > 0 && existing.length > m) {
219-
existing.warned = true;
220-
console.error('(node) warning: possible EventEmitter memory ' +
221-
'leak detected. %d %s listeners added. ' +
222-
'Use emitter.setMaxListeners() to increase limit.',
223-
existing.length, type);
224-
console.trace();
217+
++this._eventsCount;
218+
} else {
219+
if (typeof existing === 'function') {
220+
// Adding the second element, need to change to array.
221+
existing = events[type] = [existing, listener];
222+
} else {
223+
// If we've already got an array, just append.
224+
existing.push(listener);
225+
}
226+
227+
// Check for listener leak
228+
if (!existing.warned) {
229+
m = $getMaxListeners(this);
230+
if (m && m > 0 && existing.length > m) {
231+
existing.warned = true;
232+
console.error('(node) warning: possible EventEmitter memory ' +
233+
'leak detected. %d %s listeners added. ' +
234+
'Use emitter.setMaxListeners() to increase limit.',
235+
existing.length, type);
236+
console.trace();
237+
}
225238
}
226239
}
227240

@@ -254,7 +267,7 @@ EventEmitter.prototype.once = function once(type, listener) {
254267
// emits a 'removeListener' event iff the listener was removed
255268
EventEmitter.prototype.removeListener =
256269
function removeListener(type, listener) {
257-
var list, events, position, length, i;
270+
var list, events, position, i;
258271

259272
if (typeof listener !== 'function')
260273
throw new TypeError('listener must be a function');
@@ -267,17 +280,18 @@ EventEmitter.prototype.removeListener =
267280
if (!list)
268281
return this;
269282

270-
length = list.length;
271-
position = -1;
272-
273-
if (list === listener ||
274-
(typeof list.listener === 'function' && list.listener === listener)) {
275-
delete events[type];
276-
if (events.removeListener)
277-
this.emit('removeListener', type, listener);
278-
283+
if (list === listener || (list.listener && list.listener === listener)) {
284+
if (--this._eventsCount === 0)
285+
this._events = {};
286+
else {
287+
delete events[type];
288+
if (events.removeListener)
289+
this.emit('removeListener', type, listener);
290+
}
279291
} else if (typeof list !== 'function') {
280-
for (i = length; i-- > 0;) {
292+
position = -1;
293+
294+
for (i = list.length; i-- > 0;) {
281295
if (list[i] === listener ||
282296
(list[i].listener && list[i].listener === listener)) {
283297
position = i;
@@ -289,8 +303,12 @@ EventEmitter.prototype.removeListener =
289303
return this;
290304

291305
if (list.length === 1) {
292-
list.length = 0;
293-
delete events[type];
306+
list[0] = undefined;
307+
if (--this._eventsCount === 0) {
308+
this._events = {};
309+
return this;
310+
} else
311+
delete events[type];
294312
} else {
295313
spliceOne(list, position);
296314
}
@@ -312,10 +330,15 @@ EventEmitter.prototype.removeAllListeners =
312330

313331
// not listening for removeListener, no need to emit
314332
if (!events.removeListener) {
315-
if (arguments.length === 0)
333+
if (arguments.length === 0) {
316334
this._events = {};
317-
else if (events[type])
318-
delete events[type];
335+
this._eventsCount = 0;
336+
} else if (events[type]) {
337+
if (--this._eventsCount === 0)
338+
this._events = {};
339+
else
340+
delete events[type];
341+
}
319342
return this;
320343
}
321344

@@ -329,19 +352,20 @@ EventEmitter.prototype.removeAllListeners =
329352
}
330353
this.removeAllListeners('removeListener');
331354
this._events = {};
355+
this._eventsCount = 0;
332356
return this;
333357
}
334358

335359
listeners = events[type];
336360

337361
if (typeof listeners === 'function') {
338362
this.removeListener(type, listeners);
339-
} else if (Array.isArray(listeners)) {
363+
} else if (listeners) {
340364
// LIFO order
341-
while (listeners.length)
365+
do {
342366
this.removeListener(type, listeners[listeners.length - 1]);
367+
} while (listeners[0]);
343368
}
344-
delete events[type];
345369

346370
return this;
347371
};

0 commit comments

Comments
 (0)