events: optimize adding and removing of listeners
authorBrian White <mscdex@mscdex.net>
Wed, 11 Feb 2015 22:00:12 +0000 (17:00 -0500)
committerBen Noordhuis <info@bnoordhuis.nl>
Wed, 11 Feb 2015 22:06:26 +0000 (23:06 +0100)
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: https://github.com/iojs/io.js/pull/785
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Evan Lucas <evanlucas@me.com>
lib/events.js

index 33bcc18..02e7ac9 100644 (file)
@@ -30,8 +30,10 @@ EventEmitter.init = function() {
     }
   }
 
-  if (!this._events || this._events === Object.getPrototypeOf(this)._events)
+  if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
     this._events = {};
+    this._eventsCount = 0;
+  }
 
   this._maxListeners = this._maxListeners || undefined;
 };
@@ -115,15 +117,18 @@ function emitMany(handler, isFn, self, args) {
 EventEmitter.prototype.emit = function emit(type) {
   var er, handler, len, args, i, events, domain;
   var needDomainExit = false;
+  var doError = (type === 'error');
 
   events = this._events;
-  if (!events)
-    events = this._events = {};
+  if (events)
+    doError = (doError && events.error == null);
+  else if (!doError)
+    return false;
 
   domain = this.domain;
 
   // If there is no 'error' event listener then throw.
-  if (type === 'error' && !events.error) {
+  if (doError) {
     er = arguments[1];
     if (domain) {
       if (!er)
@@ -189,39 +194,47 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
     throw new TypeError('listener must be a function');
 
   events = this._events;
-  if (!events)
+  if (!events) {
     events = this._events = {};
-  else {
+    this._eventsCount = 0;
+  } else {
     // To avoid recursion in the case that type === "newListener"! Before
     // adding it to the listeners, first emit "newListener".
     if (events.newListener) {
       this.emit('newListener', type,
-                typeof listener.listener === 'function' ?
-                listener.listener : listener);
+                listener.listener ? listener.listener : listener);
+
+      // Re-assign `events` because a newListener handler could have caused the
+      // this._events to be assigned to a new object
+      events = this._events;
     }
     existing = events[type];
   }
 
-  if (!existing)
+  if (!existing) {
     // Optimize the case of one listener. Don't need the extra array object.
     existing = events[type] = listener;
-  else if (typeof existing !== 'function')
-    // If we've already got an array, just append.
-    existing.push(listener);
-  else
-    // Adding the second element, need to change to array.
-    existing = events[type] = [existing, listener];
-
-  // Check for listener leak
-  if (typeof existing !== 'function' && !existing.warned) {
-    m = $getMaxListeners(this);
-    if (m && m > 0 && existing.length > m) {
-      existing.warned = true;
-      console.error('(node) warning: possible EventEmitter memory ' +
-                    'leak detected. %d %s listeners added. ' +
-                    'Use emitter.setMaxListeners() to increase limit.',
-                    existing.length, type);
-      console.trace();
+    ++this._eventsCount;
+  } else {
+    if (typeof existing === 'function') {
+      // Adding the second element, need to change to array.
+      existing = events[type] = [existing, listener];
+    } else {
+      // If we've already got an array, just append.
+      existing.push(listener);
+    }
+
+    // Check for listener leak
+    if (!existing.warned) {
+      m = $getMaxListeners(this);
+      if (m && m > 0 && existing.length > m) {
+        existing.warned = true;
+        console.error('(node) warning: possible EventEmitter memory ' +
+                      'leak detected. %d %s listeners added. ' +
+                      'Use emitter.setMaxListeners() to increase limit.',
+                      existing.length, type);
+        console.trace();
+      }
     }
   }
 
@@ -254,7 +267,7 @@ EventEmitter.prototype.once = function once(type, listener) {
 // emits a 'removeListener' event iff the listener was removed
 EventEmitter.prototype.removeListener =
     function removeListener(type, listener) {
-      var list, events, position, length, i;
+      var list, events, position, i;
 
       if (typeof listener !== 'function')
         throw new TypeError('listener must be a function');
@@ -267,17 +280,18 @@ EventEmitter.prototype.removeListener =
       if (!list)
         return this;
 
-      length = list.length;
-      position = -1;
-
-      if (list === listener ||
-          (typeof list.listener === 'function' && list.listener === listener)) {
-        delete events[type];
-        if (events.removeListener)
-          this.emit('removeListener', type, listener);
-
+      if (list === listener || (list.listener && list.listener === listener)) {
+        if (--this._eventsCount === 0)
+          this._events = {};
+        else {
+          delete events[type];
+          if (events.removeListener)
+            this.emit('removeListener', type, listener);
+        }
       } else if (typeof list !== 'function') {
-        for (i = length; i-- > 0;) {
+        position = -1;
+
+        for (i = list.length; i-- > 0;) {
           if (list[i] === listener ||
               (list[i].listener && list[i].listener === listener)) {
             position = i;
@@ -289,8 +303,12 @@ EventEmitter.prototype.removeListener =
           return this;
 
         if (list.length === 1) {
-          list.length = 0;
-          delete events[type];
+          list[0] = undefined;
+          if (--this._eventsCount === 0) {
+            this._events = {};
+            return this;
+          } else
+            delete events[type];
         } else {
           spliceOne(list, position);
         }
@@ -312,10 +330,15 @@ EventEmitter.prototype.removeAllListeners =
 
       // not listening for removeListener, no need to emit
       if (!events.removeListener) {
-        if (arguments.length === 0)
+        if (arguments.length === 0) {
           this._events = {};
-        else if (events[type])
-          delete events[type];
+          this._eventsCount = 0;
+        } else if (events[type]) {
+          if (--this._eventsCount === 0)
+            this._events = {};
+          else
+            delete events[type];
+        }
         return this;
       }
 
@@ -329,6 +352,7 @@ EventEmitter.prototype.removeAllListeners =
         }
         this.removeAllListeners('removeListener');
         this._events = {};
+        this._eventsCount = 0;
         return this;
       }
 
@@ -336,12 +360,12 @@ EventEmitter.prototype.removeAllListeners =
 
       if (typeof listeners === 'function') {
         this.removeListener(type, listeners);
-      } else if (Array.isArray(listeners)) {
+      } else if (listeners) {
         // LIFO order
-        while (listeners.length)
+        do {
           this.removeListener(type, listeners[listeners.length - 1]);
+        } while (listeners[0]);
       }
-      delete events[type];
 
       return this;
     };