Simple Publication And Subscription Functionality (Pub/Sub) With jQuery
In the past, I've talked about how awesome jQuery's DOM-based event management is. In fact, I've even played around with using the DOM-based event management to power an object-based bind and trigger mechanism. As you saw in that exploration, however, porting the jQuery event framework onto a non-DOM context requires a good bit of finagling. At this year's jQuery Conference (2010), one of the most frequently discussed topics was that of light weight Pub/Sub (publish and subscribe). This brand of event binding is like jQuery's event binding; but, it circumvents a lot of the processing that a DOM-based event framework needs to perform. Since this seemed to be the direction in which people were moving, I thought it was time to try it out for myself.
As Rebecca Murphey pointed out in her jQCon presentation, there's an existing jQuery plugin that provides pub/sub in six lines of code. But, if you've been following my blog for any amount of time, you'll probably know that I like to learn something by writing 200 lines of code in order to figure out why those six lines of code rock. And, that's exactly what I've done here.
In the following experiment, I've created three extensions to the jQuery namespace:
$.subscribe( eventType, subscriber, callback ) - This subscribes the given subscriber to the given event type. When the event type is triggered, the given callback will be executed in the context of the subscriber (ie. "this" refers to the subscriber within the callback).
$.unsubscribe( eventType, callback ) - This unsubscribes the given callback from the given event type. Since functions are comparable objects, the subscriber is not required - only the callback that it defined.
$.publish( publisher, eventType, data ) - This allows the given publisher to publish the given event type with the given data array (if any). When the publication event is created, the publisher is defined as the "target" of the event.
Unlike traditional jQuery event binding, this light weight publication and subscription mechanism is not associated with any given jQuery collection. As such, I am requiring the publishers and the subscribers to pass themselves along with their related method calls. This is not something that I saw the pub/sub presentation demos doing; however, knowing who fired off a given event just seems kind of necessary to me. It makes the pub/sub API a little less elegant but, I think it will be worth it in the long run.
When an event gets published, an event object gets created with the following properties:
type - The event type (string) that has been published.
target - The object that has published the event.
data - An array of any additional data that has been published along with the event. Each item in this array gets passed to the subscriber callback as an additional invocation argument.
result - This is the return value of the most recently executed subscriber callback (or undefined if no value was returned).
This event object is capable of some very light-weight propagation logic. If you look at the event object that gets created above, you'll see that it stores the result of each subscriber that gets notified of a given event. If any of the subscriber callbacks return false, the $.publish() method will treat this as a request to stop immediate propagation. As such, it will break out of the $.each() iteration method that it uses to invoke all subscribers to the given event.
Furthermore, once all of the subscribers have been notified, the event object is then returned to the event publisher. At that point, the event publisher has the opportunity to alter its default behavior based on the result value of the event. There is nothing in the pub/sub model that requires any particular action to be taken place; however, if the publisher sees that the last known result is "false", then it can chose not to complete the event that it just published.
Now that you have an idea of where I am going with this, let's take a look at some code. In the following demo, I am defining the three pub/sub jQuery extensions - subscribe, unsubscribe, and publish. Then, I am creating two Person objects that automatically subscribe to the global event, "oven.doneBaking." Then, I create an oven object that announces the event, "oven.doneBaking." Naturally, the person objects will react to this event.
<!DOCTYPE html>
<html>
<head>
<title>Simple jQuery Publication / Subscription</title>
<script type="text/javascript" src="./jquery-1.4.3.js"></script>
</head>
<body>
<h1>
Simple jQuery Publication / Subscription
</h1>
<script type="text/javascript">
// Define the publish and subscribe jQuery extensions.
// These will allow for pub-sub without the overhead
// of DOM-related eventing.
(function( $ ){
// Create a collection of subscriptions which are just a
// combination of event types and event callbacks
// that can be alerted to published events.
var subscriptions = {};
// Create the subscribe extensions. This will take the
// subscriber (context for callback execution), the
// event type, and a callback to execute.
$.subscribe = function( eventType, subscriber, callback ){
// Check to see if this event type has a collection
// of subscribers yet.
if (!(eventType in subscriptions)){
// Create a collection for this event type.
subscriptions[ eventType ] = [];
}
// Check to see if the type of callback is a string.
// If it is, we are going to convert it to a method
// call.
if (typeof( callback ) == "string"){
// Convert the callback name to a reference to
// the callback on the subscriber object.
callback = subscriber[ callback ];
}
// Add this subscriber for the given event type..
subscriptions[ eventType ].push({
subscriber: subscriber,
callback: callback
});
};
// Create the unsubscribe extensions. This allows a
// subscriber to unbind its previously-bound callback.
$.unsubscribe = function( eventType, callback ){
// Check to make sure the event type collection
// currently exists.
if (
!(eventType in subscriptions) ||
!subscriptions[ eventType ].length
){
// Return out - if there's no subscriber
// collection for this event type, there's
// nothing for us to unbind.
return;
}
// Map the current subscription collection to a new
// one that doesn't have the given callback.
subscriptions[ eventType ] = $.map(
subscriptions[ eventType ],
function( subscription ){
// Check to see if this callback matches the
// one we are unsubscribing. If it does, we
// are going to want to remove it from the
// collection.
if (subscription.callback == callback){
// Return null to remove this matching
// callback from the subsribers.
return( null );
} else {
// Return the given subscription to keep
// it in the subscribers collection.
return( subscription );
}
}
);
};
// Create the publish extension. This takes the
// publishing object, the type of event, and any
// additional data that need to be published with the
// event.
$.publish = function( publisher, eventType, data ){
// Package up the event into a simple object.
var event = {
type: eventType,
target: publisher,
data: (data || []),
result: null
};
// Now, create the arguments that we are going to
// use to invoke the subscriber's callback.
var eventArguments = [ event ].concat( event.data );
// Loop over the subsribers for this event type
// and invoke their callbacks.
$.each(
subscriptions[ eventType ],
function( index, subscription ){
// Invoke the callback in the subscription
// context and store the result of the
// callback in the event.
event.result = subscription.callback.apply(
subscription.subscriber,
eventArguments
);
// Return the result of the callback to allow
// the subscriber to cacnel the immediate
// propagation of the event to other
// subscribers to this event type.
return( event.result );
}
);
// Return the event object. This event object may have
// been augmented by any one of the subsrcibers.
return( event );
};
})( jQuery );
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// I am the person class.
function Person( name ){
// Store the name property
this.name = name;
// Subscribe to the oven events.
$.subscribe( "oven.doneBaking", this, "watchOven" );
};
// Define the person class methods.
Person.prototype = {
watchOven: function( event, food ){
// Log that we have eaten foodz!!!
console.log(
"Nom nom nom -", this.name,
"is hungry for", food
);
// Return false - this will prevent other oven
// watchers from stealing my pie!
return( false );
}
};
// Create two girls, both of which will be watching the
// oven for some freshly baked pie.
var joanna = new Person( "Joanna" );
var tricia = new Person( "Tricia" );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Now, create an oven object that will publish a baking
// done event.
var oven = {
content: "Pumpkin Pie"
};
// Publishe the baking done event. Notice that we are passing
// in the oven reference to so that the event can have a
// valid event target.
//
// NOTE: The publish method returns the event object that was
// passed to all of the subscribers.
var event = $.publish(
oven,
"oven.doneBaking",
[ oven.content ]
);
// Log as to whether or not the event was propagated. Notice
// that we are using triple equals === to make sure the
// result is actually false and not some other falsey value.
console.log(
"The event was",
((event.result === false) ? "NOT" : ""),
"propagated!"
);
// Log the event as it exists after all of the subscribers
// have had a chance (if possible) to react to it.
console.log( event );
</script>
</body>
</html>
As you can see in this code, the Person constructor automatically subscribes to the event, "oven.doneBaking." However, in the callback that it uses to subscribe to the event, the Person class is returning false. As such, the first person to subscribe to the event will stop propagation of the event, thereby preventing the second person from knowing anything about the very very delicious pumpkin pie (this is typically my strategy of choice at Thanksgiving).
Because of this propagation logic, running the code leaves us with the following console output:
Nom nom nom - Joanna is hungry for Pumpkin Pie
The event was NOT propagated!
NOTE: I have not included the logged event object as it is a complex value.
In this demo, I am not making any final use of the event object other than to check the propagation status. However, if my publish request was made inside of a true domain object, I could have used the event.result value to react in different ways (much in the same way that a Form won't submit if its default behavior is prevented).
The jQuery library is really the first thing that has truly gotten me to think about event-based communication; it's bind() and trigger() methods make publication and subscription extremely easy for DOM-based functionality. However, as client-side application architecture gets more complex, it seems that people are using lighter-weight pub/sub approaches that work with plain-old Javascript objects. This past jQuery Conference has really got my machinery firing full blast!
Want to use code from this post? Check out the license.
Reader Comments
Ben, great to see you this weekend and this is a great writeup of pubsub. However, I'm not sure about the fact that you're having your subscribed functions return a value that is then available to the publisher -- what would happen, for example, if you had more than one subscriber for a published topic?
Part of the goal of pubsub is to allow that sort of extensibility -- to make it so that multiple, arbitrary pieces of your application can respond to a topic -- and to decouple the components, so one literally doesn't have to know or care about the other. A better pattern might be to have the second component broadcast another topic if something interesting happens in it; alternately, you might write your first component so it really didn't have to care how the second component reacted to the topic published by the first.
This is a small detail about an otherwise great article. Thanks for digging in and writing it :)
@Rebecca,
Great to see you too as well. I think the YayQuery team, in general, rocked the talks :)
As far as the result of an event, this is what the core jQuery Event object does:
http://api.jquery.com/event.result/
I was just trying to stick with the "best practices" outlined by the core library, albeit in the most light-weight way possible. There is nothing in this framework that tells the publisher *how* to use that result - it can do with it what it wants.
I thought about this in the same way that one might bind to a Form element in the DOM. If an event listener *outside* of the Form returns false (or calls event.preventDefault()), the Form chooses not to complete its submissions. This allows components to interact by indirect message-passing, so to speak.
Clearly, this approach makes sense for DOM interaction otherwise, we wouldn't be able to create such rich client-side application. So the question then becomes, does the DOM-centric approach also apply to a non-DOM-centric approach?
The answer: I have no idea :D I'm just trying to wrap my head around all of this stuff.
@Ben,
Yeah, I think it's important to understand how this pattern is fundamentally different and more lightweight than the standard event pattern. While it can be implemented using custom events, it shouldn't necessarily bring along all of the concepts of them when creating it from scratch, so I'm not sure how much it matters how jQuery implements events.
In my mind, it's definitely critical to the pattern that more than one subscriber be able to connect to a topic; I suppose you could write an implementation that would check the return values of all subscribers, and then return false to the publisher if any of them returned false, but at that point I think the implementation could start to stray beyond the simplicity that I love about pubsub in general -- it is very much fire and forget.
Regarding your form example, the idea in pubsub is that the form view would publish the data from the form, and then any subscribed components would have the chance to react to it. The form view's job would be done once it published that data; it would never need to know what happened next. If the form needed to be able to be hidden, then the form view could react to another published topic by hiding itself, but the hiding of it would occur independently of the publishing of the data. The form view would absolutely *not* be responsible for getting the form data to the server -- that task should fall to a service.
Hope this helps :)
@Rebecca,
I agree that the two context are very different; however, I think it's important to clarify our terminology. When we say "light-weight", we are talking about the cost of processing, not the robustness of the feature-set, correct? Otherwise, I can't see any reason why one would opt for less functionality at the same cost, especially when that functionality is well encapsulated.
So, even with some pseudo behavior/propagation functionality, this approach is still "light-weight."
Once we differentiate between the concepts of light-weight and robust, then the question becomes whether or not the robust features that we've included are valid or not.
As far as more than one subscriber being able to listen for an event type, this is still the case. Remember, if all the event listeners simply "listen", then everything goes ahead as planned. It is only when one of the listeners actually takes the initiative to return an actually False value that immediate propagation is stopped. So it's not like we are limited to only one listener; it's just that we have the ability for one listener to alter the control flow.
And, I think this is really the point that is not sitting well with you - allowing a listener to alter the greater control flow.
Going back to the Form example in our non-DOM context, I am going to agree with you. In fact, I am having trouble coming up with a good example of where I would actually want to stop an event from being fully processed.
This is all so new and exciting :) I am going to put my thinking cap on and see what I can come up with. Thanks for all the wonderful thought-food.
Ben, interesting stuff! I started to write a comment on two reasons why trying to prevent event propagation isn't viable, but it expanded itself into a blog post involving parallel universes: http://www.halhelms.com/blog/index.cfm/2010/10/18/Preventing-Event-Propagation-in-EventDriven-Programming.
Which jQuery plugin are you talking about that is 6 lines of code?
@Hal,
Awesome, going to read it right now :)
@Drew,
I am not sure, I will have to defer to @Rebecca to answer that.
Hey guys, I tried to codify my thoughts in a more "Real world" example of how propagation logic might be used for fun and profit:
www.bennadel.com/blog/2038-Simple-Neural-Net-jQuery-Pub-Sub-Experiment-With-Propagation-Logic.htm
I use "real world" in quotes because its obviously a silly example; but, one that I hope will lend itself as a platform for more use-case based discussions.
I wasn't there, but I imagine she may be talking about the plugin Peter Higgins wrote
http://higginsforpresident.net/projects/
http://higginsforpresident.net/js/static/jq.pubsub.js
and the fact that bocoup showed the performance difference using jQuery events vs. the pub/sub plugin.
http://weblog.bocoup.com/publishsubscribe-with-jquery-custom-events
@Michael,
Ah, very cool - thanks for the links. I've heard Bocoup talked up a lot this weekend at jQCon. Looks like they are doing some high quality stuff.
I'm sitting here, staring at the screen, trying to wrap my head around all this pub/sub stuff. I think what's going to be most difficult for me is to figure out what kind of stuff should use pub/sub and which should use more of a direct invocation.
Also, I was wondering if perhaps we could clarify what makes the DOM so different than anything else. After all, it was sitting here for years, not being leveraged. And now, with its event model, we are able to build on top of it to create robust applications. What makes the DOM special in that way that doesn't apply to other types of systems?
I would say that it is the extremely generic nature of HTML UI elements. But, isn't part of the event architecture an attempt to make things more generic?
@Rebecca,
Going back to your SRCHR example from your presentation - how is the SRCHR form "widget" that you could reuse on various pages different than the HTML Form element that you can reuse on different pages? Why should one offer propagation control while the other does not?
Thanks all - this stuff is really hard for me to understand.
Hi Ben,
I've been involved with pubsub for many years in the area of Comet and "later" with Dojo. It's an eventing paradigm that's sorely missing from JS.
Anyway, I would strongly recommend looking at phiggins' plug-in as it's pretty elegant, and based on the work we've done with pubsub in Dojo.
The DOM isn't really special except that DOM events are expensive from a performance perspective relative to simple JS calls. In Dojo our normal event system works with both DOM events and function to function bindings. PubSub takes this to the next level by allowing event connections, registrations, publishes, and subscribes to happen with no two publishers or subscribers knowing or caring anything about each other.
As far as pubsub goes, the problem I see in your suggested implementation is that with returning a value, potentially false, it breaks the pubsub paradigm. Basically it should not be possible with pubsub for one subscriber to cancel the publish to another subscriber. What you're really doing instead is binding a set of events to happen in order, which is a different (and perhaps useful) pattern.
The format for pubsub topics is typically like a url fragment, e.g. "oven/bake/". The reason for this is that in Comet systems, you can actually set permissions on who can subscribe and publish to each topic, hierarchically, and have your calls be RESTful. It's a nice convention that's easy to follow and
Additionally, something I use a lot is Dojo's connectPublisher method. What this does is allow you to take an existing method and have it publish a topic whenever it is called, without modifying the source of that method with an explicit publish call.
For example, you have (in pseudo-code:
Oven = {}
Oven.bake()
Person = {}
Person.eat{}
subscribe("oven/bake", Person, "eat")
connectPublisher("oven/bake", Oven, "bake")
This allows you to easily place a pubsub scaffolding over existing code and events which is very handy.
I hope this helps... it's cool that jQuery is becoming interested in pubsub. There's also a cometD jQuery library that might be useful to check out as well, though that's intended for events to and from a Comet server.
Regards,
-Dylan
@Dylan,
The connectPublisher() is interesting. I assume this uses some sort of aspect oriented programming (AOP) where is wraps the original function inside of one that publishes an event after it executes. AOP is new to me, but I am liking the idea. How do you handle unsubscribing? Do you use a jQuery proxy() approach where a GUID is transferred to the outer method for comparison?
My biggest mental block is having two unrelated modules affect each others actions. If you look at my next post, you'll see in the comments that I come up against this issue a LOT and I simply can't wrap my head around it.
I think I might just see if I can hire someone to teach me how to deal with this :)
@Ben: yes, we use an AOP approach. For connectPublisher, we return a handle which can be disconnected through dojo.disconnect() So in the case of registering a publisher in this way, we do retain a reference to it just for disconnect purposes.
As far as two competing approaches (connect/bind vs. pubsub), we just live with them possibly competing with each other, and we don't worry too much about the consequences because both are useful in certain situations. It can definitely become more challenging to wrap your head around, especially during debugging.
@Dylan,
I think maybe my problem is that I am trying too hard to decouple two things that are necessarily coupled. After all, if the state of one UI widget affects the functionality of another one, then aren't these two things, by definition, coupled?
The kind of concept I come back to over and over again is the GMail navigation where it will say something like:
"Are you sure you want to leave the page? Your message will be lost."
I keep trying to think of a way that this can be achieved via pub/sub... hence my preventing of default events. But, maybe that just doesn't make sense? Maybe these two UI elements (navigation and compose email area) are necessarily coupled?
@Ben: onunload is somewhat of a special case because you have to cancel stuff right away to do anything.
In thinking about it, such an event is really something you would either handle within your message bus (the thing that routes all the pubsub calls), or some sort of hook in your pubsub router that it could connect to. In this case, it is something that ideally is pubsub-ing with wildcard topics, meaning all of them.
Perhaps there's a better pattern, but I think the proper handling of canceling the unload event, and then some way to interrupt all publish notifications until deciding what to do with the unload action could work.
Anyway, those are my scattered thoughts.
@Dylan,
From this back and forth, as well as from my conversation with other people, I am beginning to get the feeling that any eventing model in which the bubbling / propagating of events has "meaning" is thought of as a "special" case. I am OK with this - it actually might help me. I keep trying to reconcile the difference between the eventing presented by the DOM and the pub/sub eventing that people like Rebecca and you are talking about. But perhaps that is just trying to compare apples and oranges (albeit, apples and oranges that have the same name).
Perhaps these two types of eventing can just live side-by-side in peace.
This morning, I just installed Node.js for the first time to play with that and that architecture seems to use a somewhat different model of eventing. From what I read (which was limited), it seems they are using Event "emitters". So, rather than having a centralized event model or an object-specific event model, they have instances of these event emitters that you can bind/trigger events on.
Of course, it also appears that a number of objects in the node.js framework extend the event emitter class so as to be able to have object-specific event binding.
Eventing... it's so hot right now :)
Ben,
I know this is ancient, but here are some notes:
* With global events, wildcard support is essential to enable cross-cutting concerns.
Pitfall: Memory leaks if you add too many event listeners (especially problematic in collections).
Pitfall solution: Refactor to delegate events for collections. Consider the singleton pattern for collections which need to listen for events per-item.
Pitfall: Too many events / too much chatter on the global event bus
Solution: Consider moving some communication to local objects or mediators
* Local event emitters (like Node's EventEmitter) can be used both globally (using a single, globally accessible event emitter), or local per-instance event emitters which must be coordinated manually, via tight coupling (booo), mediators (slightly better), or dependency injection.
Pitfall: Potentially lots of manual wiring and unnecessary code complexity
Solution: Switch to global events, or wrap event mediation with a factory
Pitfall: Tight coupling
Solution: Global events, mediators, or dependency injection
Pitfall: Too many mediators
Solution: Global events
* DOM-style tree structures are (infrequently) useful for propagating events up and down a tree of object nodes. You can implement that by having all the nodes inherit a prototype which includes an event emitter. A mediator can bind all events on each node and retrigger on the parent for bubbling (bottom up), or on each child for capture (top down). In this situation, it becomes useful to stop event propagation.
In general, this isn't a one-size-fits all issue. Use the right tool for the job. I haven't event mentioned message queues and stacks, or message services (an SOA component). All of these solutions have their place in a large application.
DOM-style tree structures are (infrequently) useful for propagating events up and down a tree of object nodes. You can implement that by having all the nodes inherit a prototype which includes an event emitter. A mediator can bind all events on each node and retrigger on the parent for bubbling (bottom up), or on each child for capture (top down). In this situation, it becomes useful to stop event propagation.