Event emitters provide the decoupling between the caller and the callee. A typical use would be (taken from Node.js Events and EventEmitter) is "ringing" a door bell
var events = require('events');
var eventEmitter = new events.EventEmitter();
function ringBell()
{
console.log('ring ring ring');
}
eventEmitter.on('doorOpen', ringBell);
eventEmitter.emit('doorOpen');
// prints "ring ring ring"
Event emitters to me always had two limitations
They do not return a result to the caller. Thus I could not do something like this
function ringBell() { return 'ring ring ring'; } eventEmitter.on('doorOpen', ringBell); console.log('door opened', eventEmitter.emit('doorOpen'));
While they are asynchronous, there is no simple way of ordering or knowing when the
emit
has finished. I would love to be able to use promise-like syntaxeventEmitter.emit('doorOpen') .then(function () { console.log('door has opened'); });
Let us convert EventEmitter to return a promise to be resolved with result of the on
listener.
To do this, we will use my self-addressed library for converting postMessage
calls to return a promise. The principle behind self-addressed
is that it puts data (call it letter)
into an envelope. The envelope has a stamp that allows us to later match some other message to the
original data, resolving the promise. In our case, let us overwrite the emit
method first.
var events = require('events');
var selfAddressed = require('self-addressed');
var eventEmitter = new events.EventEmitter();
var _emit = events.EventEmitter.prototype.emit;
eventEmitter.emit = function (name, data) {
function mailman(address, envelope) {
_emit.call(address, name, envelope);
}
return selfAddressed(mailman, this, data); // returns a promise
};
The self-addressed
uses the post office delivery analogy everywhere. When we want to send
a new piece of data, we use the function selfAddressed
that calls the custom delivery mechanism mailman
and a given address
. This is necessary to decouple our promise-returning layer from the actual
message passing.
Once the data has been delivered, it is hidden inside an envelope. To open an envelope we need
to use selfAddressed
again. We will do this inside on
handler, wrapping the user-supplied
event listener.
var _on = events.EventEmitter.prototype.on;
eventEmitter.on = function (name, fn) {
function onSelfAddressedEnvelope(envelope) {
// we could get data from envelope if needed
var result = fn();
selfAddressed(envelope, result);
// somehow deliver the envelope back to the caller .emit
}
_on.call(this, name, onSelfAddressedEnvelope);
};
At this point, the callback functio has been called, and the result has been placed back into
the envelope using selfAddressed(envelope, result);
statement. We use the same function to perform
every step because self-addressed
has been lucky
enough to only need the number of arguments to determine the desired action.
In order to send the envelope back to the caller and fulfill the promise, we need to hack the
self-addressed
semantics. Typically we would just use same approach as in the emit
function -
by sending the envelope using mailman
function. This maps well to the cross-frame communication, but
EventEmitters are unidirectional - there is no sending the response back to whoever called
eventEmitter.emit
method. Thus we just mark the envelope as the reply and call selfAddressed
to deliver it, taking a shortcut.
var _on = events.EventEmitter.prototype.on;
eventEmitter.on = function (name, fn) {
function onSelfAddressedEnvelope(envelope) {
if (selfAddressed.is(envelope)) {
var result = fn();
selfAddressed(envelope, result);
// there is nowhere to send the response envelope
// event emitters are unidirectional.
// so open the envelope right away!
envelope.replies = 1;
selfAddressed(envelope); // deliver
}
}
_on.call(this, name, onSelfAddressedEnvelope);
};
That is it. Now we can use the event emitter as a promise returning asynchronous operation.
function ringBell(x)
{
log('ringing bell', x);
return 'ring ring ring';
}
eventEmitter.on('doorOpen', ringBell);
eventEmitter.emit('doorOpen').then(function (sound) {
log('door has opened with', sound);
});
log('after emitted an event');
The output shows the expected sequence of events
after emitted an event
ringing bell
door has opened with ring ring ring
Related: Journey from procedural to reactive JavaScript with stops.