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
    LightFace:  Facebook Lightbox for MooTools

    One of the web components I've always loved has been Facebook's modal dialog.  This "lightbox" isn't like others:  no dark overlay, no obnoxious animating to size, and it doesn't try to do "too much."  With Facebook's dialog in mind, I've created LightFace:  a Facebook lightbox...

  • By
    9 Mind-Blowing WebGL Demos

    As much as developers now loathe Flash, we're still playing a bit of catch up to natively duplicate the animation capabilities that Adobe's old technology provided us.  Of course we have canvas, an awesome technology, one which I highlighted 9 mind-blowing demos.  Another technology available...

Incredible Demos

  • By
    MooTools, Mario, and Portal

    I'm a big fan of video games. I don't get much time to play them but I'll put down the MacBook Pro long enough to get a few games in. One of my favorites is Portal. For those who don't know, what's...

  • By
    MooTools Zebra Tables Plugin

    Tabular data can oftentimes be boring, but it doesn't need to look that way! With a small MooTools class, I can make tabular data extremely easy to read by implementing "zebra" tables -- tables with alternating row background colors. The CSS The above CSS is extremely basic.

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

    • Rantie

      And that’s good confusion, right thinking most likely )

    • Rantie

      Yep, but you don’t need indexes, you can search CB and remove by actual index. Otherwise it will lead to huge arrays, you don’t really delete element.

  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.

  17. Johan

    Nice. With modern browsers it would be the same to use the following code?:

    var event = new Event('build');
    
    // Listen for the event.
    elem.addEventListener('build', function (e) { ... }, false);
    
    // Dispatch the event.
    elem.dispatchEvent(event);
    
    

    Or what would be the difference?

  18. Rantie

    Why do you remove array element like that (instead splice)? It’s coold that you don’t search calback, but this will lead to huge arrays, nope?

    a = [1,2,3]
    (3) [1, 2, 3]
    delete a[1]
    true
    a
    (3) [1, empty, 3]
    
  19. Rantie

    BTW, that’s the difference between this “pub/sub” implementation and “Mediator” pattern?

    Here is my mediator pattern implementation:

    export default function Mediator (obj) {
      const channels = {}
    
      const mediator = {
        subscribe: function (channel, cb) {
          if (!channels[channel]) {
            channels[channel] = []
          }
    
          channels[channel].push(cb)
    
          return this.unsubscribe.bind(null, channel, cb)
        },
    
        unsubscribe: function (channel, cb) {
          const i = channels[channel].indexOf(cb)
    
          if (i >= 0) {
            channels[channel].splice(i, 1)
    
            if (!channels[channel].length) {
              delete channels[channel]
            }
    
            return true
          }
    
          return false
        },
    
        publish: function (channel) {
          if (!channels[channel]) {
            return false
          }
    
          const args = Array.prototype.slice.call(arguments, 1)
    
          channels[channel].forEach(subscription => {
            subscription.apply(null, args)
          })
    
          return this
        },
      }
    
      return obj ? Object.assign(obj, mediator) : mediator
    }
    
  20. Chris

    Oldie but goodie, was just about to use dojo pub/sub, but we will be discontinuing use of dojo in the future so a pure JS approach is nice. Ashamed I didn’t build it myself ;)

  21. Aleks

    Hey, complete stranger passing through from the interwebz: thanks a bunch for posting this. It’s elegant, clean and useful. I was literally going to roll my own and this saved me.

  22. Somone

    10,000 opinions in the comment stream. SMH

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