Pub/Sub JavaScript Object

By  on  

There are three keys to effective AJAX-driven websites:  event delegation, History management, and effective app-wide communication with pub/sub.  This blog employs of all of these techniques, and I thought I'd share the simplest of them:  a tiny pub/sub module I use on this site.

If you've not used pub/sub before, the gist is that you publish to a topic and anyone can subscribe, much like the way a radio works: a radio station broadcasts (publishes) and anyone can listen (subscribes).  This is excellent for highly modular web applications; it's a license to globally communicate without attaching to any specific object.

The JavaScript

The module itself is super tiny but massively useful:

var events = (function(){
  var topics = {};
  var hOP = topics.hasOwnProperty;

  return {
    subscribe: function(topic, listener) {
      // Create the topic's object if not yet created
      if(!hOP.call(topics, topic)) topics[topic] = [];

      // Add the listener to queue
      var index = topics[topic].push(listener) -1;

      // Provide handle back for removal of topic
      return {
        remove: function() {
          delete topics[topic][index];
        }
      };
    },
    publish: function(topic, info) {
      // If the topic doesn't exist, or there's no listeners in queue, just leave
      if(!hOP.call(topics, topic)) return;

      // Cycle through topics queue, fire!
      topics[topic].forEach(function(item) {
      		item(info != undefined ? info : {});
      });
    }
  };
})();

Publishing to a topic:

events.publish('/page/load', {
	url: '/some/url/path' // any argument
});

...and subscribing to said topic in order to be notified of events:

var subscription = events.subscribe('/page/load', function(obj) {
	// Do something now that the event has occurred
});

// ...sometime later where I no longer want subscription...
subscription.remove();

I use pub/sub religiously on this website and this object has done me a world of good.  I have one topic that fires upon each AJAX page load, and several subscriptions fire during that event (ad re-rendering, comment re-rendering, social button population, etc.).  Evaluate your application and see where you might be able to use pub/sub!

Recent Features

  • By
    Creating Scrolling Parallax Effects with CSS

    Introduction For quite a long time now websites with the so called "parallax" effect have been really popular. In case you have not heard of this effect, it basically includes different layers of images that are moving in different directions or with different speed. This leads to a...

  • By
    5 More HTML5 APIs You Didn’t Know Existed

    The HTML5 revolution has provided us some awesome JavaScript and HTML APIs.  Some are APIs we knew we've needed for years, others are cutting edge mobile and desktop helpers.  Regardless of API strength or purpose, anything to help us better do our job is a...

Incredible Demos

  • By
    MooTools-Like Element Creation in jQuery

    I really dislike jQuery's element creation syntax. It's basically the same as typing out HTML but within a JavaScript string...ugly! Luckily Basil Goldman has created a jQuery plugin that allows you to create elements using MooTools-like syntax. Standard jQuery Element Creation Looks exactly like writing out...

  • By
    MooTools ASCII Art

    I didn't realize that I truly was a nerd until I could admit to myself that ASCII art was better than the pieces Picasso, Monet, or Van Gogh could create.  ASCII art is unmatched in its beauty, simplicity, and ... OK, well, I'm being ridiculous;  ASCII...

Discussion

  1. How are you managing your queue? I see the “remove” method, but not how it’s used. Is the page lifetime short enough that you’re just not bothering with any kind of lifespan or garbage collection?

    • Good question Erryn — I’ve updated the code sample to show the removal cycle.

  2. Ron

    Nice and tiny :-)
    Is this similar to signals then? I use signalsjs a lot but never bothered to look inside.

  3. Shouldn’t the remove handle look like this?

    var index = topics[topic].queue.push(listener) - 1;
    
    return (function(topic, index) {
      return {
        remove: function() {
          delete topics[topic].queue[index];
        }
      }
    })(topic, index);
    

    https://gist.github.com/phusick/3eec19b56cb7c8e82ce4

    • This way it works as expected, i.e. it only removes a single subscription, not all the subscriptions of the topic, but the array is constantly growing as delete only sets array item to undefined and doesn’t affect array length.

    • Good point — I updated my post with a forEach as well!

  4. rud

    You should check Keeto’s Company, it´s a great MooTools pub/sub I’ve been using for quite some time.. pub/sub truely shapes your code and logic in a much nicer and context agnostic way…

  5. How about:

    remove: function() {
        topics[topic].queue.splice(index, 1);
    }
    
  6. splice would change indices of the array and therefore remove() wouldn’t work.

    • jemcik

      sorry, my mistake
      presence of undefined elements in array confuses me a little

  7. Chandra Veera

    Good one. Couple of minor improvements,
    1. For better readability, instead of naming the array ‘queue’, I would name it ‘listeners’.
    2. In the comment, you had mentioned “Provide handle back for removal of topic “. I think it should have been “Provide handle back for removal of a listener for a topic”. You are actually removing a listener from the array not a topic.

  8. One note of warning, something that spans ALL patterns is proper utilization. I’ve seen a number of scripts where observer patterns were utilized and somewhere along the line good practice went out the window.

    It becomes REALLY easy to just start pub’ing and sub’ing all the things and suddenly you have subscriptions responding to things they were never intended to.

    Using good event naming conventions or implementing something like PostalJS (https://github.com/postaljs/postal.js) are great for preventing unwieldy event subscriptions.

    • gh83

      I agree, I’m working on a project that’s a proper example of pub/sub gone bad. When your subscribers are publishing their own things in response to other publications, it’s easy to lose control over the flow of the program and you have to work extra hard to avoid circularity.

  9. nice little snippet **but** :P

    1. the closure with the return is very pointless … you have already variables defined in the outer closure per each invoke, why that wrap? just misleading/confusing

    2. this is a very good case for either an Object.create(null) or the usage of hasOwnProperty and actually as hasOwnProperty.call(topics, topic) instead of topics.hasOwnProperty(topic) otherwise you have a very weak pub/sub logic

    var events = (function(){
      var topics = {};
      var hOP = topics.hasOwnProperty;
    
      return {
        subscribe: function(topic, listener) {
          // Create the topic's object if not yet created
          if(!hOP.call(topics, topic)) topics[topic] = [];
    
          // Add the listener to queue
          var index = topics[topic].push(listener) -1;
    
          // Provide handle back for removal of topic
          return {
            remove: function() {
              delete topics[topic][index];
            }
          };
        },
        publish: function(topic, info) {
          // If the topic doesn't exist, or there's no listeners in queue, just leave
          if(!hOP.call(topics, topic)) return;
    
          // Cycle through topics queue, fire!
          topics[topic].forEach(function(item) {
          		item(info||{});
          });
        }
      };
    })();
    

    3. using queue property … not sure why is that useful, but having a sparse Array is not the best thing on a long run … remember when index is 2^32 it flips back, I’d rather use indexOf to never set same listener twice and drop it at the right index

    • Emanuel Tannert

      Hi Andrea,

      sorry, I accidentally submitted my last comment before I was done writing :)

      My Question is: why exactly do we have to use hOP.call(topics, topic) instead of just invoking hOP(topic)? Isn’t hOP already bound to the topics Object? Which detail am I missing here?

      Thanks,
      Emanuel

  10. Hi David,

    I’ve been using pubsub for a long time too, its really helped me to build large JS applications in the past. I think I’m now running into limitations with the pattern now and was hoping you may have solved this issue.

    I’ve found pubsub to be excellent for apps with data that flows in one direction, so with an AJAX app like you mentioned above, where various bits of the page are updated once every minute from an AJAX request to the server.

    But now I’m trying to build an application that allows you to create, edit and update existing content in an admin application. This requires me to request data and respond to that data, passing the data back and forth between different modules in the app.

    This has resulted in me writing what I can only call “pubsub tennis”, here’s an example:

    news.pubsub.on('request', function () {
          news.pubsub.emit('respond', [stuff]);
    });
    

    If you take this example and chain a couple of modules into it, all doing a publish in a subscribe, you can see where I’m going with this.

    Its horrible, I’ve removed some of this complexity by building each module in backbone.js (so messaging within the module itself doesn’t use the global pubsub event system) but I still need to play pubsub tennis when communicating between each module – so I essentially haven’t solved the issue.

    Do you have any advice you can give me? Have you ran into pubsub tennis before?

    Thanks,
    Tom

  11. David, your publish can’t pass 0 as data to subscriptions. I found this out when selecting row 0 in a list.

    publish: function(topic, info) {
          // If the topic doesn't exist, or there's no listeners in queue, just leave
          if(!topics[topic] || !topics[topic].queue.length) return;
    
          // Cycle through topics queue, fire!
          var items = topics[topic].queue;
          items.forEach(function(item) {
          		item((info !== undefined) ? info : {});
          });
        }
    

    What we really want to know if is anything was sent or not. Also, for your consideration, what do you think about about a subscribeOnce method?

    subscribeOnce : function(topic, listener) {
    			// Create the topic's object if not yet created
    			if(!topics[topic]) topics[topic] = { queue: [] };
    
    			// Add the listener to queue
    			var index = topics[topic].queue.push(function(data){
    					listener(data);
    					delete topics[topic].queue[index];
    				}) -1;
    
    			// Provide handle back for removal of topic
    			return {
    				remove: function() {
    					delete topics[topic].queue[index];
    				}
    			};
    		}
    
    

    I use subscribeOnce sometimes for convenience.

  12. What is the code doing? Please explain so others can understand. Can understand whats pub and sub. But I am unable to understand that the what code is doing and what are its objectives. Can you please explain in bit more clarity so it is useful for every one?

    thanks/

  13. Garvin

    Great article as always, David. One thing. Pretty sure this:

      item(info != undefined ? info || {}); // whoops: || where : is expected
     

    should be this:

      item(info != undefined ? info : {}); // OCD resolution
     
  14. Mike Collins

    What about checking for deleted listeners?

    Shouldn’t this:

    topics[topic].forEach(function(item) {
    	item(info != undefined ? info : {});
    });
    

    be changed to this:

    topics[topic].forEach(function(item) {
    	item && item(info != undefined ? info : {});
    });
    
  15. satyagraha

    Interesting article & discussion. FWIW, I note that PubSubJS (https://github.com/mroderick/PubSubJS) is strongly of the view that broadcasts should be async, i.e. each pub is put on the end of the scheduler queue with setTimeout(fn, 0).

    Also, consider executing the pubs from a copy of the subscriber list to avoid any unexpected issues with (un)subscribe operations which might occur during publishing.

  16. Don

    Pubsub is a very messy pattern, you’ll regret it in any serious app.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!