4
\$\begingroup\$

This seems to work, but for some reason I'm not entirely sure about it.

Can anyone point out any issues with this simple pub/sub library? Thank you!

(function (undefined) {
    window.mylib = $.extend(window.mylib, (function () {

        var subscriptionsBag = function() {
            var self = this;
            self.count = -1;
            self.handlers = {};

            var cancel = function(index) {
                var self = this;
                delete self.handlers[index];
            };

            self.add = function (fn) {
                if (typeof(fn) !== "function") {
                    throw new Error("function expected.");
                }

                var index = ++self.count;
                self.handlers[self.count] = fn;

                return {
                    cancel: cancel.bind(self, index)
                };
            };

            self.fire = function(obj) {
                for (var handlerKey in self.handlers) {
                    if (!self.handlers.hasOwnProperty(handlerKey)) {
                        continue;
                    }
                    var handler = self.handlers[handlerKey];
                    if (!handler) continue;
                    handler(obj);
                }
            };
        };

        var subscriptions = {
            "": {}
        };

        var publish = function(e, obj) {
            var evt = "@" + e;
            var subs = subscriptions[evt];
            if (!subs) return;

            subs.fire(obj);
        };

        var subscribeTo = function (e, fn) {
            var evt = "@" + e;
            var subs = subscriptions[evt] = (subscriptions[evt] || new subscriptionsBag());
            return subs.add(fn);
        };

        var events = {
            publish: publish,
            subscribeTo: subscribeTo
        };

        return {
            events: events
        };
    })());
})();
\$\endgroup\$
3
\$\begingroup\$

Member names

The code is confusing because it involves neither event dispatch nor event handling - therefore is nothing to do with events - yet there are member names evt and events, where :

  • the evt vars are actually uids used as object keys
  • the events object contains references to methods.

The loose-coupled nature of Pub/Sub does indeed tempt you into believing that it involves events. But no, it does not (at least typically not, and certainly not here). Whereas a Pub and its corresponding Subs can be in completely diffferent parts of the code base, when a Pub fires it informs the subscribers synchronously, in the same event thread.

Similarly, the subscriptions object is not well named. It receives objects which can themselves be subscribed to, not subscriptions directly. If anything was to be named "subscriptions" it would be SubscriptionsBag's self.handlers - each handler is effectively a subscription.

Leveraging jQuery.Callbacks

You are already using jQuery - great!

The jQuery documentation includes a really neat example under jQuery.Callbacks(), showing how Callbacks() instances can be exploited for Pub/Sub.

If you were to adopt this approach, it would :

  • take all the pain out of having to write/test your own .add(), .cancel() and fire() methods
  • replace your SubscriptionsBag() with a Topic() constructor
  • replace your subscriptions repository with a topics repository
  • give you nice monads that will method-chain.

You need do little more than adopt jQuery's code and assign the Topic constructor to your chosen namespace in lieu of jQuery, and trap the topics repository in a closure (or otherwise avoid the global namespace).

(function (lib, topics) {
    lib.Topic = function Topic (id) {
        var callbacks,
            topic = id && topics[id];
        if (!topic) {
            callbacks = jQuery.Callbacks();
            topic = {
                publish: callbacks.fire,
                subscribe: callbacks.add,
                unsubscribe: callbacks.remove
            };
            if (id) {
                topics[id] = topic;
            }
        }
        return topic;
    };
})(window.mylib, {});

Thus, your versions of the usage examples in the jQuery documentation would be :

// Subscribers
window.mylib.Topic( "mailArrived" ).subscribe( fn1 );
window.mylib.Topic( "mailArrived" ).subscribe( fn2 );
window.mylib.Topic( "mailSent" ).subscribe( fn1 );

// Publishers
window.mylib.Topic( "mailArrived" ).publish( "hello world!" );
window.mylib.Topic( "mailSent" ).publish( "woo! mail!" );

And an example involving asynchronously derived data :

// Subscriber
var mailArrivedTopic = window.mylib.Topic("mailArrived").subscribe(fn1); // Note how the subscribe() method returns the Topic instance (monadic behaviour).

// Publisher
$.ajax({...}).then(mailArrivedTopic.publish);
\$\endgroup\$
2
  • \$\begingroup\$ Very nice. I didn't know about jQuery Callbacks. I think I'll be following your suggestions and implement accordingly. Thank you! \$\endgroup\$ – Raine Nov 29 '15 at 9:43
  • 1
    \$\begingroup\$ Raine, thank you. Callbacks are very robust. They have been used by jQuery internally since the outset. At some point they decided to expose them for direct use but not many people seem to have discovered them. \$\endgroup\$ – Roamer-1888 Nov 29 '15 at 14:58
2
\$\begingroup\$
$.extend

There it goes. You're using jQuery. jQuery has it's own built-in event system. It has an on for listening events, and trigger for firing events. It even supports "channels" (event namespacing). If you proceed with using jQuery, then you might as well use the one it has.

(function (undefined) {
  window.mylib = $.extend(window.mylib, (function () {

You're also wrapping everything in unnecessary IIFEs. A single one would have been fine.

(function(namespace){

  namespace.foo = function(){...} //namespace.foo === mylib.foo

// This translates to "mylib is equals to mylib or create one if none"
}(this.mylib = this.mylib || {}));

I think you over-engineered your solution. You can simply have an object whose keys are the event names and the value is an array. Subscribing is simply just pushing the function into the correct event name's array. Publishing is simply looking for that key (which is as simple as obj[evt]) and iterating through the array.

Given the above, the entire pub-sub mechanism can be simplified to:

(function(namespace){

  var subscriptions = {};

  namespace.publish = function(event){
    // everything after event name gets passed to the handler
    var args = Array.prototype.slice.call(arguments, 1);
    var handlers = subscriptions[event] || [];
    handlers.forEach(function(handler){
      handler.apply(null, args);
    });
  };
  namespace.subscribe = function(event,fn){
    if(!subscriptions.hasOwnProperty(event)) subscriptions[event] = [];
    subscriptions[event].push(fn);
  };

}(this.mylib = this.mylib || {}));
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks for your input. I'm not sure about the performance implications of using jQuery's built-in event system (even though your point about reuse is well taken). Your solution, as far as I can tell, does not support subscriber removal (mine does). I like the arguments slicing and handler fn application. \$\endgroup\$ – Raine Nov 23 '15 at 13:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.