The line between Events and Promises
In January 2014, but please bear with me this post will talk about way more updated things, I tweeted something like:
if you understand how $(document).ready(callback) works in jQuery, you basically understood the main concept behind ES6 Promises #truestory
— Andrea Giammarchi (@WebReflection) January 29, 2014
There are few comments that misunderstood that tweet, and I’ve pushed yesterday a module that rather explains what is all this about, so here my explanation.
What did that actually mean?
The cool part about events is that these can be dispatched/triggered/notified at any time.
We add a listener to any kind of event type, and we wait in a non blocking way.
// a common nodejs Emitter case
generic.on('any:event-type', function () {
// Yeah, I got notified !!!!
});
// a common DOM handler case
generic.addEventListener('any:event-type', function (e) {
// Yeah, `e.type` notified me !!!
});
Events are the best solution for repeated events, and other cases, including:
- notifications about any kind of progress
- pushed notifications (same listener run N notifications/times)
- every use case when we’d like to be able to stop listening to an event
- quickly repeated events ( mouse/touch-move )
And probably more … however, Events suck big time at one thing:
- if the notification already happened, we gonna wait forever!
This is true for well known events, in the DOM world, such DOMContentLoaded
, on load
, even on beforeunload
or on unload
.
This is also actually true for anything that might have been already clicked and disappeared from the view.
The solution to this “already happened“ problem? Promises!
What Promises can do that Events cannot?
Metaphorically speaking, Promises are the equivalent of any event that will remove itself once executed, and will be triggered once if set after the event already occurred, receiving the value that such event carried.
Confusing, uh? So here an example, in terms of functionality, of what an Event looks like, if it’d like to act like a Promise.
// the simplified Promise approach
// resolved at first click
var onceClicked = new Promise(
link.addEventListener.bind(link, 'click')
);
// the simulated Event approach
// (PLEASE DON'T DO THIS AT HOME)
link.addEventListener('click', function click(e) {
// it has been clicked once
// we need to trap the current method
// so we can use it later on for other events
var aEL = link.addEventListener;
// we need to remove this handler
link.removeEventListener(e.type, click);
// we redefine the method in order
// to be able to control it later on
link.addEventListener = function (type) {
// whoever decided to listen to something
// will be registered like it should be by default
aEL.apply(link, arguments);
// however, if this was a click, and since
// this already happened
if (type === 'click') {
// we create each time a new event
// 'cuase we cannot dispatch the old one
var ce = new CustomEvent(type), k;
// ignoring things that cannot be copied
for (k in e) {
try {
ce[k] = e[k];
} catch (meh) {
/*\_(ツ)_*/
}
}
// and finally dispatch it
link.dispatchEvent(ce);
// remembering to remove the very same lisnter after
link.removeEventListener.apply(link, arguments);
}
};
});
If you are thinking “What The Hack?“ rigth now I am with you: too many side effects and problems.
On top of that, if the user already clicked all that effort will be vane.
Truth is, even with the promise approach if the element was already clicked we won’t be notified, but the point here is that these two patterns are effectively different and if it takes that much to be Promise like for events, it takes same amount of code, if not more, and whenever is actually possible, to simulate Events through Promises.
How about the best from both patterns?
Indirectly, involuntarily, and probably quite unexpectedly, the initial tweet I’ve mentioned was exactly about this: jQuery
library, capable of understanding at runtime if the document was ready or not, has been somehow providing the behavior of a Promise, invoking the callback when the document would have become ready, or instantly, in case the ready state was already understood and resolved.
To be fair though, those were different times so actually .ready(callback)
cannot be compared to a Promise, since there’s no .catch()
or rejection channel, and it also cannot be compared to Events, since there’ no way, once you define what you are waiting for, to come back.
Last, but not least, while we can create Promises just about anything we need, and we can also create Events for any sort of type, jQuery
unique channel where such behavior was expected was the ready
one.
In any case, the jQuery problem-solving/pragmatism won over any sort of argument: finally nobody had to care anymore about the status of the document so that anything loaded before, during, or after its status change, could simply be notified when the time is just about right!
Augmenting the handy pattern
I’ve been experimenting the when method via eddy-js for quite a while, and while I found it extremely handy, I’ve always felt it wasn’t that intuitive.
What I’ve actually recently achieved, in an absolute non breaking, fully backward/forward compatible way, is a little script which aim is to solve only this very specific issue: notify from the past, the present, or the future!
// the object is called `notify`
// it lets us add any listener, at any time
notify.when('data-is-ready', function (data) {
// OK, we've got some data here!
console.log(data);
});
// it doesn't matter "when" data arrives
// whoever will resolve the Event 'data-is-ready'
// will notify possible listeners waiting for it
notify.that('data-is-ready', {the: 'data'});
// but what if our listener ask for data too late?
// well, the resolved data object will be sent around too
notify.when('data-is-ready', function (data) {
// instant data, I love it!
console.log(data); // {the: 'data'}
});
The naming convention of the API is hopefully self explicative:
notify.when(type, handler)
will ask to be notified whenever data is availablenotify.that(type, one[, orMore[, values]])
will notify all listeners waiting for data
Please note at the beginning I have used .about
and now it’s just an alias for .that
which is more imperative, as suggested in this first comment.
How about we get notified later on, in a proper asynchronous fashion?
Well, this is the main and only overload of the .that
method:
// in node
fs.readFile(
'my-setup.json',
notify.that('fs.read:my-setup.json')
);
// before, after, or during the operation
notify.when(
'fs.read:my-setup.json',
function (err, json) {
if (err) console.error('damn it');
else {
var result = JSON.parse('' + json);
console.log('Yayyyy, JSON:', result);
}
}
);
So how does this notify
object manage handlers, work, exactly?
- you can wait for any sort of event at any time through the
.when(type, cb)
method ( Events like ) - if the event already happened, you’ll be instantly notified ( Promise like )
- if the event never happened, you’ll be notified whenever that happens ( Events / Promise like )
- if you’ve been notified for an event already, you’ll never be notified again ( Promise like )
- if you want to be notified again in case something resolves the event again, you can always add explicitly through the
.when(type, cb)
method your refreshed new behavior ( Events / Promise like ) - if you changed your mind about such Event notification for whatever reason, you can use
.drop(type, cb)
at any time ( Events like ) - if you actually want a private channel, you can either use a unique id or a
Symbol
, as event type, or create a newnotification
channel through the method.new()
This, and other details about the module, in the official notify-js repository.
Which use case it this pattern useful for?
Since new patterns are usually heavier to digest, and we might end up overusing them or describing them like Barney did (a pretty accurate description if we have no context):
@WebReflection repeating promises! Anachronistic events! Love it!
— Barney Carroll (@barneycarroll) August 14, 2015
It’s good to understand why, when, and how this pattern could be handy.
Following, few use cases that are a very good fit for this pattern and are very hard to solve otherwise with just Events or Promises:
- as module loader, like a simplified AMD,
notify.when(library, cb)
can be used to receive any sort of object or function once available. A singlenotify.that(library, {my:'lib'})
call, and we can forget aboutasync
scripts,load
events, or global scope polling in order to know if a library is available or not - as ask privileges once filter, where
notify.when('geoposition:available', cb)
will be resolved only when the user will eventually confirm the will to be tracked, and the very first position could be sent vianotify.that('geoposition:available', evt.coords)
- as global JSONP listener, instead of polluting the global, we could instrument our REST API to return the content like
notify.that("$_GET['type']", null, json_encode($result))
sending optionally anError
instead of null, if something went wrong - as generic trigger for any action that occurs sporadically but it might need a global communication channel: it doesn’t have to traverse the entire DOM or be dispatched through events, it can be simplified as notification (pub/subscribe substitute)
To keep in mind that every notification happens once, and only once, and if an event is triggered again only those that eventually re-registered as listeners will be notified again.
And here, all cases where notify
would be a bad choice:
- any sort of repeated Event, just use events in that case
- something that needs to change frequently, ‘cause the fact
notify.that(type, par1, parN)
can be invoked more than once per event instead of throwing if already resolved, is to cover edge cases I haven’t thought about yet, and not to abuse it, ‘cause it’s an anti-pattern - any case where a Promise would be more appropriate, since Promises do not necessarily expose the ability to be resolved, while
notify
is all about trusting the communication channel (Symbol, unique-id, or Event name) and its behavior (once resolved we trust its resolved value)
The only case where using notify
instead of a Promise would make sense, is in very outdated browsers between IE6 and IE11, where we might not want/need a full polyfill, and keep it simple with a script which size is less than a half of a KB.
Enjoy!