Since the dawn of JavaScript, browsers have allowed event handlers to be attached to DOM elements like so:
| link.onclick = clickHandler; |
Ah, simplicity itself! There’s just one caveat: if you wanted two click handlers for an element, you’d have to aggregate them yourself with a wrapper function.
| link.onclick = function() { |
| clickHandler1.apply(this, arguments); |
| clickHandler2.apply(this, arguments); |
| }; |
Not only is this tedious, it’s also a recipe for bloated, all-purpose handler functions. That’s why the W3C added addEventListener to the DOM specification in 2000 and jQuery abstracted it with the bind method. bind makes it easy to add as many handlers as you like to any event on any element (or set of elements), without worrying about stepping on anyone else’s toes.
| $(link) |
| .bind('click', clickHandler1) |
| .bind('click', clickHandler2); |
(In jQuery 1.7+, the new on syntax is preferred over bind.[26] There’s also the click method, which is a shorthand for bind(’click’, ...); however, I prefer to consistently use bind/on.)
From a software architecture perspective, jQuery publishes the link element’s events to anyone who wants to subscribe. That’s why it’s called “PubSub.”
The old-style DOM event API, where binding to event meant writing object.onevent = ..., is now largely forgotten in favor of PubSub. The architects of Node’s API liked PubSub so much that they decided to include a generic PubSub entity called EventEmitter that other objects can inherit from. Just about every source of I/O in Node is an EventEmitter: file streams, HTTP servers, and even the application process itself. To wit:
Distributed/processExit.js | |
| ['room', 'moon', 'cow jumping over the moon'] |
| .forEach(function(name) { |
| process.on('exit', function() { |
| console.log('Goodnight, ' + name); |
| }); |
| }); |
Countless stand-alone PubSub libraries exist for the browser. In addition, many MVC frameworks like Backbone.js and Spine provide their own EventEmitter-like modules. We’ll talk more about Backbone later in this chapter.
Let’s use Node’s EventEmitter as an example of a PubSub interface. It has a simple, nearly minimal design.
To add an event handler to an EventEmitter, just call on with the event type and the handler.
| emitter.on('evacuate', function(message) { |
| console.log(message); |
| }); |
The emit method will call all handlers for the given event type. For instance, the following
| emitter.emit('evacuate'); |
would call all evacuate handlers.
Note that the term event here has nothing to do with the event queue. See Synchronicity.
You can add any number of additional arguments when you emit an event. All arguments are passed to all handlers.
| emitter.emit('evacuate', 'Woman and children first!'); |
There are no restrictions on event names, though the Node docs offer a useful convention.
Typically, event names are represented by a camel-cased string.[27]
All of EventEmitter’s methods are public, but it’s common convention for events to be emitted only from “inside” the EventEmitter. That is, if you have an object that inherits the EventEmitter prototype and uses this.emit to broadcast events, its emit method shouldn’t be called elsewhere.
PubSub implementations are so simple that we can create one in about a dozen lines of code. The only state we need to store is a list of handlers for each event type we support.
| PubSub = {handlers: {}} |
When we add a listener, we push it to the end of the array (which means that listeners will always be called in the order in which they were added).
| PubSub.on = function(eventType, handler) { |
| if (!(eventType in this.handlers)) { |
| this.handlers[eventType] = []; |
| } |
| |
| this.handlers[eventType].push(handler); |
| return this; |
| } |
Then when an event is emitted, we loop through all of our handlers.
| PubSub.emit = function(eventType) { |
| var handlerArgs = Array.prototype.slice.call(arguments, 1); |
| for (var i = 0; i < this.handlers[eventType].length; i++) { |
| this.handlers[eventType][i].apply(this, handlerArgs); |
| } |
| return this; |
| } |
That’s it. We’ve just implemented the core of Node’s EventEmitter. (The only major things we’re missing are the ability to remove handlers and to attach one-time handlers.)
Of course, PubSub implementations vary slightly feature-wise. When the jQuery team noticed that several different PubSub implementations were being used throughout the library, they decided to abstract them with $.Callbacks in jQuery 1.7.[28] Instead of using an array to store the handlers corresponding to an event type, you could use a $.Callbacks instance.
Many PubSub implementations parse the event string to provide special features. For example, you may be familiar with namespaced events in jQuery: if I bind events named "click.tbb" and "hover.tbb", I can unbind them both by simply calling unbind(".tbb"). Backbone.js lets you bind handlers to the "all" event type, causing them to go off whenever anything happens. Both jQuery and Backbone let you bind or emit multiple event types simultaneously by separating them with spaces, e.g., "keypress mousemove".
Although PubSub is an important technique for dealing with async events, there’s nothing inherently async about it. Consider this code:
| $('input[type=submit]') |
| .on('click', function() { console.log('foo'); }) |
| .trigger('click'); |
| console.log('bar'); |
The output is
<= | foo |
| bar |
proving that the click handler was invoked immediately by trigger. In fact, whenever a jQuery event fires, all of its handlers will be executed sequentially without interruption.
So, let’s be clear: when the user clicks the Submit button, that’s an async event. The first click handler fires from the event queue. But the event handler has no way of knowing whether it’s being run from the event queue or from your application code.
If too many handlers fire in sequence, you risk blocking the thread and making the browser unresponsive. Worse, if events are emitted from event handlers, they can easily create an infinite cycle.
| $('input[type=submit]') |
| .on('click', function() { |
| $(this).trigger('click'); // stack overflow! |
| }); |
Think back to the word processor example at the start of this chapter. When a user presses a key, many things need to happen, and several of them require complex calculations. Doing them all before returning to the event queue would be a recipe for an unresponsive app.
A good solution to this problem is to maintain a queue of things that don’t need to happen right away and use a timed function to run the next task in the queue periodically. A first attempt might look something like this:
| var tasks = []; |
| setInterval(function() { |
| var nextTask; |
| if (nextTask = tasks.shift()) { |
| nextTask(); |
| }; |
| }, 0); |
(We’ll learn about a more sophisticated approach to job queuing in Dynamic Async Queuing.)
PubSub makes it easy to name, distribute, and stack events. Anytime it makes intuitive sense for an object to announce that something has happened, PubSub is a great pattern to use.
18.222.48.24