public
Last active

jQuery Tiny Pub/Sub: A really, really, REALLY tiny pub/sub implementation for jQuery.

  • Download Gist
jquery.ba-tinypubsub.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
/*!
* jQuery Tiny Pub/Sub - v0.4 - 1/4/2011
* http://benalman.com/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
 
(function($){
 
var o = $({});
 
$.subscribe = function( type, data, fn ) {
if ( $.isFunction( data ) || data === false ) {
fn = data;
data = undefined;
}
 
function proxy() {
return fn.apply( this, Array.prototype.slice.call( arguments, 1 ) );
};
 
proxy.guid = fn.guid = fn.guid || proxy.guid || $.guid++;
 
o.bind( type, data, proxy );
};
 
$.unsubscribe = function() {
o.unbind.apply( o, arguments );
};
 
$.publish = function() {
o.trigger.apply( o, arguments );
};
 
})(jQuery);
jquery.ba-tinypubsub.min.js
JavaScript
1 2 3 4 5 6 7 8 9
/*
* jQuery Tiny Pub/Sub - v0.4 - 1/4/2011
* http://benalman.com/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
(function($){var o=$({});$.subscribe=function(type,data,fn){if($.isFunction(data)||data===false){fn=data;data=undefined}function proxy(){return fn.apply(this,Array.prototype.slice.call(arguments,1))}proxy.guid=fn.guid=fn.guid||proxy.guid||$.guid++;o.bind(type,data,proxy)};$.unsubscribe=function(){o.unbind.apply(o,arguments)};$.publish=function(){o.trigger.apply(o,arguments)}})(jQuery);

This is the Original jQuery Tiny Pub/Sub plugin updated for jQuery 1.7 (which makes it EVEN SMALLER because bind and unbind are replaced with on and off)

Note: Ignore the first argument passed to your subscribed callbacks (the jQuery event object).

Another Note: Previous versions (v0.4+) were written in an attempt to remove the first argument and create a more future-proof API, but unfortunately this resulted in much less elegant, larger and slower code. The point of this plugin is to be TINY, to be used in situations where only size (not performance or usability) is the primary concern (tweets, code golf, etc).**

I frequently see comments about how jQuery's events system has unnecessary overhead that precludes it from being used as the core of a Pub/Sub implementation. The jQuery events system is tried-and-true, having been architected to be both fast and robust, and the vast majority of users of this plugin should never encounter any kind of performance issues.

Because this plugin's $.subscribe, $.unsubscribe and $.publish methods all use the jQuery .on(), .off() and .trigger() methods internally, those methods' complete signatures are available to you.

You can use namespaces for more control over unsubscribing and publishing.

Just use this handy terminology guide (jQuery events term → Pub/Sub term), and everything should make sense:

  • on → subscribe
  • off → unsubscribe
  • trigger → publish
  • type → topic

In addition, should you need it, these methods are fully compatible with the jQuery.proxy() method, in case you not only want more control over to which context the subscribed callback is bound, but want to be able to very easily unsubscribe via callback reference.

Regarding performance: If at some point, your application starts processing so many messages that performance issues start to develop, you could always write a replacement "jQuery Not-So-Tiny Pub/Sub" plugin with the same API and just drop it in as a replacement to this plugin. But keep in mind that you'll also need to add in the aforementioned features, too.

// Super-basic example:

function handle(e, a, b, c) {
  // `e` is the event object, you probably don't care about it.
  console.log(a + b + c);
};

$.subscribe("/some/topic", handle);

$.publish("/some/topic", [ "a", "b", "c" ]);
// logs: abc

$.unsubscribe("/some/topic", handle); // Unsubscribe just this handler

// Or:

$.subscribe("/some/topic", function(e, a, b, c) {
  console.log(a + b + c);
});

$.publish("/some/topic", [ "a", "b", "c" ]);
// logs: abc

$.unsubscribe("/some/topic"); // Unsubscribe all handlers for this topic

Here's a working example using jQuery 1.7 (edge).

Cool, Ben. Thanks. I'm going to substitute this for my homebrew "signals", as mine doesn't have an "unsubscribe" yet. Come on over to github.com/timeglider some time and have a look around. I'll probably implement that table parser you whipped up next week.

ionutzp, I'd recommend checking out the MDC apply documentation, which explains in detail how .apply works.

In general, .apply gives you the ability to invoke a function, specifying the context in which it will execute (this inside the function) as well as specifying the arguments to be passed, but as an array. To make a long story short, I'm using .apply here to execute one object's method as if it were called as a method of a different object.

shit, i got carried away; thanks

This is awesome! Your implementation is clean as a whistle, and I'm glad it's making its way to the jQuery Core.
Two questions though:

  1. What's the reason for not passing the event object to the handler? Is it completely unnecessary/creates performance issues?
  2. You speak of performance issues that may arise with a high volume of messages. Do you think this can be neatly avoided by using something as simple as a priority queue?

I thought this was very slow too, since it is DOM based. Here's jsperf comparing to higgins, with latest update to the final gist I could find for jQuery 1.6 which had the following optimization $({}) instead of $('<b/>'). This increases performance 100%. The original test /1 already had this optimization, and now I know why! http://jsperf.com/pub-sub-implementations/2

Drew, in making this into a "plugin" version that can be used with any version of jQuery, $({}) actually causes errors in some versions of jQuery, so I changed it to $('<b/>') (see the comments). Frankly, what was once very small and elegant has turned into a big, sloppy mess.

If you want "small and elegant" go for version 0.3 and just note that the event object is being passed as the first argument to the event handler!

This was linked as inclusion to the core, so I didn't give much attention to support in older versions of jQuery. The wrapper code should go away as well, or implemented differently to meld better with the core code, right?

It would be nice to have something that works today and will work this time next year (when our product ships). We're using $().bind currently, I thought it would be too slow and still have that concern. Using $({}) and $({<b/>}) provided better performance than Higgins, $({}) provided much better performance though.

Just a note @drewwells, the test you were linking to was faulty. I've updated it here: http://jsperf.com/pub-sub-implementations/3

EDIT:
Here's an alternative solution: https://gist.github.com/826794

I updated the jsperf to have the alternative solution for comparison as well: http://jsperf.com/pub-sub-implementations/6

Update: If you want the original TINY pub/sub plugin, use jQuery Tiny Pub/Sub v0.3 and just ignore the first argument passed to your callbacks (which is an event object). Versions v0.4+ were written in an attempt to remove that first argument and create a more future-proof API, but unfortunately this resulted in much less elegant, larger and slower code. The point of this plugin is to be TINY, to be used in situations where only size (not performance or usability) is the primary concern (tweets, code golf, etc).

(I added this note into my original comment as well)

ben - check out https://github.com/furf/jquery-enable/blob/master/src/bindable.js

it evolved from a similar pub/sub pattern to the one your using. the chief differences are:

  • callbacks fire in scope of originating object instead of a jQuery object (maintaining a similar behavior to jQuery events),
  • custom event binding methods, ie. 'onMyEvent',
  • can bind to all events triggered by an object
  • usable on a prototype to extend all instances

note: contains a couple methods available in the containing library

@furf - Bindable is closer traditional event "binding" than actual pub/sub.

The pub/sub pattern consists of two things: topics and messages. Topics are subscribed to by arbitrary listeners, and messages are received when that topic publishes a message. With event systems, events are bound to and fired from contexts (such as HTML elements), like the Bindable example you linked to.

The whole point of pub/sub is that it doesn't assume a context for it's topics/messages. Think of it like an application-wide notification system.

@bentruyman understood. i was just posting as a comparison. personally i use bindable not only on classes for custom events, but also the application level for handling app-wide pub/sub. i prefer it because i often have multiple apps on the same page and i can scope the pub/sub to an individual app instead of the global (which this basic pub/sub does) without need for unnecessarily deep namespacing. think of it as a var for pub/sub.

This rules, thanks

Nice, Ben. Very elegant. Here's the same with a tiny bit more shaved off.


var o=$({});
$.each( {on:'subscribe',off:'unsubscribe',trigger:'publish'} ,function(k,v){
$[v]=function(){o[k].apply(o,arguments)};
});

A handy little plugin, thanks Ben! And even littler thanks to a-laughlin, nice work! :)

JSFiddled here: http://jsfiddle.net/HvAJf/46/

a-laughlin's is 132 characters in case anyone else is wondering!

@cowboy the minified version is wrong. it uses , instead of ; between the different functions

@mahemoff note that @a-laughlin's code, once wrapped in an IIFE (152 bytes minified), is 138 bytes gzipped, while mine (185 bytes minified) is 121 bytes gzipped.

@cowboy I figured after commenting that what really broke the plugin was the missing ; at then end, which caused issues when concatenating files together

Your concatenating utility could join JavaScript source files on ";" instead of "".

God help me how do I stop getting notifications on this thing?

works in IE9+, or with Modernizr 2.5 or standalone Function.prototype.bind polyfill

(function($) {
  var o         = $({});
  $.subscribe   = o.on.bind(o);
  $.unsubscribe = o.off.bind(o);
  $.publish     = o.trigger.bind(o);
}(jQuery));

Does anyone have any thoughts on using this pattern with Zepto? I've found that Zepto doesn't like $({}), though it's fine with $('<b/>'). Is this a significant performance hit to use $('<b/>'), or is there a better way to port this plugin to Zepto?

@eschwartz $({}) is just an empty jQuery object, substitute it for whatever is the equivalent in Zepto

@rwldrn Thanks, but my issue is that I haven't been able to figure out how to create an empty element in Zepto. I suppose I should pose that issue on a Zepto forum, but I thought someone might have a workaround here.

Zepto only makes use of the DOM-based event system so there's no way to get a real empty element to work as the global pub/sub object. We might as well use ("<b/>").

I wrapped this into AMD module to use it with require.js

define(function () {

    "use strict";

    /**
     *    Events. Pub/Sub system for Loosely Coupled logic.
     *    Based on Peter Higgins' port from Dojo to jQuery
     *    https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js
     *
     *    Re-adapted to vanilla Javascript
     *
     *    ----------------------------------------------------------
     *    And then wrapped to AMD Module by Dragan Bajcic @kodisha
     *
     *    @class Events
     */
    return {
        cache : {},
        /**
         *    Events.publish
         *    e.g.: Events.publish("/Article/added", [article], this);
         *
         *    @class Events
         *    @method publish
         *    @param topic {String}
         *    @param args    {Array}
         *    @param scope {Object=} Optional
         */
        publish : function (topic, args, /** {Object=} */ scope) {

            console.log('publish',topic, args);
            if (this.cache[topic]) {
                var thisTopic = this.cache[topic],
                    i = thisTopic.length - 1;

                for (i ; i >= 0 ; i -= 1) {
                    thisTopic[i].apply(scope || this, args || []);
                }
            }
        },
        /**
         *    Events.subscribe
         *    e.g.: Events.subscribe("/Article/added", Articles.validate)
         *
         *    @class Events
         *    @method subscribe
         *    @param topic {String}
         *    @param callback {Function}
         *    @return Event handler {Array}
         */
        subscribe : function (topic, callback) {

            console.log('subscribe', topic, callback);
            if (!this.cache[topic]) {
                this.cache[topic] = [];
            }
            this.cache[topic].push(callback);
            return [topic, callback];
        },
        /**
         *    Events.unsubscribe
         *    e.g.: var handle = Events.subscribe("/Article/added", Articles.validate);
         *        Events.unsubscribe(handle);
         *
         *    @class Events
         *    @method unsubscribe
         *    @param handle {Array}
         *    @param completly {Boolean}
         *    @return {type description }
         */
        unsubscribe : function (handle, completly) {
            var t = handle[0],
                i = this.cache[t].length - 1;

            if (this.cache[t]) {
                for (i ; i >= 0 ; i -= 1) {
                    if (this.cache[t][i] === handle[1]) {
                        this.cache[t].splice(this.cache[t][i], 1);
                        if (completly) {
                            delete this.cache[t];
                        }
                    }
                }
            }
        }
    };

});

I have only one concern/suggestion to this relatively simple PubSub - it doesn't take into account published events that have already happened (in the past). Why is this important? Assume for a second that I want to subscribe to an event that had already happened, or I dont know that it happened, but still want my new subscriber to be triggered with the last-published values? My suggestion is to add something like this:

(function($) {

var o = $({}), pastEvents = {};

$.subscribe = function() {
var type = arguments.slice(0, 1)[0],
handler = arguments.slice(-1)[0];
//Fire your subscribe handler if event has already happened
if(type in pastEvents) {
pastEvents[type].done(function() {
handler.apply(o, arguments);
});
}
//Subscribe to future events as well
o.on.apply(o, arguments);
};

$.unsubscribe = function() {
o.off.apply(o, arguments);
};

$.publish = function() {
var type = arguments.slice(0, 1)[0],
data = arguments.slice(1)[0];
//Preserve data for future subscribers to this event
pastEvents[type] = $.Deferred().resolve(data).promise();
o.trigger.apply(o, arguments);
};

}(jQuery));

Even smaller just for sillyness

var o=$({}),s='subscribe';
$.each({on:s,off:'un'+s,trigger:'publish'},function(k,v){$[v]=function(){o[k].apply(o,arguments)};});

If we're talking about relative sizes, why use "publish/subscribe" nomenclature? Just stick with "on/off/trigger" (or my preferred "on/off/do").

UPDATE I just found out why the "pros" stick to trigger instead of do (boo <IE9).

Example: https://gist.github.com/zaus/4756518

/* jQuery Tinier Pub/Sub - v0.9b - "on/off/do version" - 2013-02-11
 * original by http://benalman.com/ 10/27/2011
 * Original Copyright (c) 2011 "Cowboy" Ben Alman; Licensed MIT, GPL */

(function($) {

  // "topic" holder
  var o = $({});

  // attach each alias method
  $.each({on:0,off:0,"go":'trigger'}, function(alias,method) {
    $[alias] = function(topic, callbackOrArgs) {
        o[method || alias].apply(o, arguments);
    }
  });

}(jQuery));

Is there a way i can e.preventDefault() on passed event? I ve tried it, but it doesn't work...

@connected - what's the default event you're trying to prevent? isn't this limited to the arbitrary hidden topic var o? just curious.

@kodi thanks for the requirejs module implementation!

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.