Finally an EventEmitter that does what I want... and maybe what you want too...
- often faster than many existing event emitters: benchmarked against EventEmitter3, one of the fastest. Try it in your browser!
- more object-friendly: listeners can be objects - no need to "bind" on listening methods anymore.
- you can stop listening "per listener": no more need to store each listener function so you can remove it later.
- helps you find leaks: debug counting allows each "class" of listeners to set its own maximum number.
- helps you avoid listening to inexistant events: emitter must declare which events it can emit, hence it can generate an error as soon as you try to listen to an invalid event.
Read below for more details about these...
Besides for its speed, why would you use nice-emitter?
How many times did you want to get a method to be called on a specific event, and had to "bind" for that?
emitter.on('tap', this.myMethod.bind(this));
Cost:
- extra memory: for the binding function
- extra CPU: to create/compile the binding function, then to run it each time the event is emitted
To avoid some of the costs of "bind" you could also use a (rather ugly) self
variable:
var self = this;
emitter.on('tap', function () { self.myMethod(); });
Or with ES6:
emitter.on('tap', () => this.myMethod());
...and this worked fine until you need to remove this listening function: to be able to remove it, you had to keep it somewhere from the start. So you ended-up doing:
this.myListeningFunction = function () { self.myMethod(); };
emitter.on('tap', this.myListeningFunction);
...
emitter.off('tap', this.myListeningFunction); // off and removeListener are synonyms
When you had more than one listening function, it could quickly become cumbersome...
emitter.on('tap', this.myMethod, this); // "this" will be passed as context to myMethod
...
emitter.off('tap', this.myMethod);
Actually you have 2 more ways to stop listening:
emitter.off('tap', this); // by passing the same context ("this") given to "on" earlier
emitter.forgetListener(this); // accross ALL events for "this" context
NB: the context-passing feature is also provided by EventEmitter3 (but not the extra APIs for removal).
nice-emitter
will help you find leaks by allowing each "class" of listeners to set its own maximum number.
Ever wondered why Node.js EventEmitter sets its "leak warning" limit to 10 by default?
This could be because this counting was designed wrong from the start.
In most systems, there will be 0 or 1 listener to a given type of events (2 would mean a leak), or a dozen listeners like when UI components listen to one of them (the limit to decide a leak would vary a lot).
What seems to be true for most systems is: it is always easier to decide the right limit at listener-level than emitter-level.
Hence, nice-emitter
allows to set the limit "per listener class".
emitter.setListenerMaxCount(5, this); // "this" or any instance of listener's class
Because this way of counting is more flexible, we can now set the default limit to 1
. This means you can detect leaks much earlier: back then you might have to run through faulty code 10 times to see the warning.
And for cases where you need n
listeners of the same class A
, you can call emitter.setListenerMaxCount(n, new A())
and you will know as soon as n + 1
listeners of class A
are set. In the meantime, the limit remains 1 for your class B
(imagine you have a singleton B
).
With all EventEmitter implementations I have seen so far, you can listen to an inexistant event forever. Nothing will happen until you find your bug. This is surprising because there was a very cheap way for doing this at EventEmitter's level: simply by having each legit event declared. And yes, if you work on one of these codebases where dynamicaly generated event names are used as a kind of "late-binding", you are in trouble anyway, sorry.
var EventEmitter = require('nice-emitter');
var inherits = require('util').inherits;
/**
* A class that inherits from EventEmitter
* and emits 2 events: 'signal1' and 'signal2'
**/
function MySignaler () {
EventEmitter.call(this);
this.declareEvent('signal1');
this.declareEvent('signal2');
}
inherits(MySignaler, EventEmitter);
MySignaler.prototype.methodWhichEmits = function () {
this.emit('signal1', someParameter);
};
MySignaler.prototype.otherMethodWhichEmits = function () {
this.emit('signal2');
};
...
var signaler = new MySignaler();
//--- Somewhere else, a listener...
MyListener.prototype.startListening = function () {
signaler.on('signal1', this.method1, this);
signaler.on('signal2', this.method2, this);
};
MyListener.prototype.stopListening = function () {
signaler.forgetListener(this);
};
MyListener.prototype.method1 = function (someParameter) {
... // signaler will pass "this" as expected
};
MyListener.prototype.method2 = function () {
... // signaler will pass "this" as expected
};
(as of Node V10.8.0; doc found at https://nodejs.org/api/events.html)
The paragraphs below should help you answer the question:
"I am already using an EventEmitter in my project; can I use nice-emitter instead?"
In most cases the answer should be "YES!" and I believe you will not regret this change.
Several of these are also not handled by EventEmitter3.
In alphabetical order:
defaultMaxListeners
: this "max listeners" counting system is one of the reason why Node's EventEmitter is so clumsy to detect leaks. See how oursetListenerMaxCount
is making this easier.getMaxListeners
: let me know why you need this.listeners
: curious to see who used this and why... Maybe internal API for Node.js?prependListener
: this API could be implemented, however... (a) relying on listener subscribing order is not a sign of a good design for your code, and (b) while you are relying on this order, starting to set one listener at beginning of the line seems like looking for more trouble. Maybe you need another way to manage the order of actions, a component dedicated to this goal (which is definitely not what event emitters were created for).prependOnceListener
: such an interesting animal, combining my worries about both ordering andonce
...rawListeners
: same remark as forlisteners
above.
Special events:
- Error handling by listening to
error
events: making sense in Node.js in some server applications, not for most other cases. - Events
newListener
andremoveListener
: rarely needed.
eventNames
: a bit too "dynamic"? You can uselistenerCount(eventId)
to see how many listeners for a given eventId. If you don't know which eventId you are interested in, you are probably in a kind of trouble already.once
: since version 0.5,nice-emitter
implements aonce
API that tries to mitigate the issue of unexpected delay or missing notification. Why only worry about it foronce
and not foron
? Read more about it below...removeAllListeners
: who was supposed to call this API anyway? It looks more like a termination/cleanup method, to remove all dependencies before shutting down your app/system. If implemented later, should probably be named differently.- Order of event reception by listeners: Node.js guarantees that your listeners will receive events in the order in which they started listening. This is also true for
nice-emitter
, until you remove some of them and add others (or add them back). If you really need this order, now is a good time to ask yourself why, because your design should normally not rely on that. If you still want it this way, callEventEmitter.respectSubscriberOrder(true)
and order will be kept, at the expense of a bit more CPU and memory.
I believe that once
is an anti-pattern. I could not find good article explaining this so here are my 2 cents: if you wanted just to be notified once then you could use the regular on
and unsubscribe when your listener is called. I think the problem is more subtle and perhaps due in part to the English second meaning of the word "once": "as soon as". This encourages developers to use once
as a way to wait for a given condition. From this misuse come all the issues I have seen so far:
- The original
once
API does not let you set a timeout, so you rarely find out when it took very long for your listener to be notified, or when this notification never came. - If your app does not break from a missing notification, you can also end-up setting up a duplicate
once
listener later, and then good luck when the event comes... - When you wait for something important, instead of a timeout, you might also need an "error callback" so you can be notified that whatever you are waiting for failed for good.
What should be done, in short? Instead of using once
to react to the readiness of a service, or similar event with variable delay and outcome, we should use a specific API/component. Event emitters are not designed for this at the moment, at least not with the current specification.
See sample (above) and test/test.js
for examples of how these are used.
Declares an event for this emitter.
Event must be declared before emit
, on
, or any other event-related method is called for this event. This brings extra error checking and runtime efficiency.
Returns a "quick emitter" for a given event ID of this EventEmitter.
Using a quick emitter to emit is quite faster (if you are chasing fractions of milliseconds).
Returned object has methods emit0
, emit1
, emit2
, emit3
and emitN
. You should call quickEmitter.emit0()
if you have 0 extra parameters, and so on up to 3. If you have 4 or more parameters, you should call emitN(...)
to which you can pass a random number of parameters (but note that this is obviously less efficient).
Similar to Node.js API except when optional third parameter is used.
If listener
is passed, it will be passed as context (this
) to the listening function when the event is emitted.
This same context can also be used later to remove listeners with off
(see below).
Specifying your context when calling "on" is often much easier than having to track/store which functions you use to subscribe.
If you pass a context, nice-emitter
also checks that the same event ID is not already subscribed to with same context.
Note that if listener
is omitted, the context (this
) passed to your listening function will be the EventEmitter (same as in Node.js).
Similar to Node.js API expect there is ALWAYS a timeout. The default timeout is set to a "human-scale" duration to help you debug your code when broken, hence a few seconds only.
If you are setting a "once" listener for an event that may-or-may-not come, you probably could use on
without much trouble.
Expiration of the timeout just logs a console.error
, so in a way one could wonder what big difference it makes. Well, having to think each time about "How long should it wait?" and "What happens when it does not come on time?" will probably help you designing a more robust logic.
The optional listener
parameter works the same as in on
(see above). If you really want the Node.js way, you should pass undefined
here.
See also my rant about why once is not your friend.
Similar to Node.js API except that if second parameter is not a function, it must be the "context" or listener used when you called on
.
Unsubscribes the given listener (context) from all events of this emitter.
Sets the limit listener count for this emitter and listener class objects.
Default is 1 for all classes when this API is not called.
If the limit is passed, an error is thrown or logged, unless debug level is set to NO_DEBUG
(see setDebugLevel
below).
NB: you can pass any instance of listener's class.
Sets debug level. level
must be one of:
EventEmitter.DEBUG_THROW
: Debug checks & counting. Errors are thrown. Helps you debug your code by crashing early. This is the default level, except if code is minified.EventEmitter.DEBUG_ERROR
: Debug checks & counting. Errors go toconsole.error
but execution continues as normally as possible.EventEmitter.NO_DEBUG
: No counting, no checks except those that avoid crashes. The fastest level, with minimum memory usage. If your code is minified, NO_DEBUG is automatically the default (no need to callsetDebugLevel
).