1

I've done some pub/sub using jQuery events.

Basic functionality works:

var publish = function(name) {                        // --- works
  var args = Array.prototype.slice.call(arguments);   // convert to array
  args = args.slice(1);                               // remove arg1 (name)
  $("html").trigger(name, args);
}

var subscribe = function(name, callback) {            // --- works
  $("html").on(name, callback);
}

var unsubscribe = function(name, callback) {          // --- works
  $("html").off(name, callback);
}

// usage:                                             // --- works
var myhandler = function () { /* ... */ };
subscribe("foo", myhandler);
publish("foo", 1, false, "bob", {a:1, b:"2"});
unsubscribe("foo", myhandler);

But subscribing the way I want, does not:

var subscribe = function(name, callback) {            // --- can't unsub this
  $("html").on(name, function () {
    var args = Array.prototype.slice.call(arguments);
    args = args.slice(1);
    callback.apply(null, args);
  });
}

I want the second subscribe function, as that anonymous callback strips the first argument (an Event object) which I don't want in the "clean" client code. All I want to receive in the callback is the event data, I don't want Event object itself.

But this means that I cannot call unsubscribe successfully, as I am not passing into off() the same callback as passed into on() - which was not the actual callback, but a wrapped anonymous function.

Is there a way around this?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
h bob
  • 3,610
  • 3
  • 35
  • 51

3 Answers3

2

The usual way is to use jQuery's "namespaced" events:

$("html").on(name + ".my-pub-sub", function () {
    // ...
});

Then to unsubscribe:

$("html").off(name + ".my-pub-sub");

That tells jQuery to remove all handlers with the given event name and that specific namespace.

Here's a simplified example:

// Add two click handlers:
$("button").on("click", function() {
  $("<p>Non-namespaced click</p>").appendTo(document.body);
});
$("button").on("click.namespace", function() {
  $("<p>Namespaced click</p>").appendTo(document.body);
});

// Show they both trigger
$("<p>Clicking...</p>").appendTo(document.body);
$("button").click();

// Remove the namespaced one without a function ref
$("button").off("click.namespace");

// Now just the non-namespaced one runs
$("<p>Clicking again...</p>").appendTo(document.body);
$("button").click();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<button>The button</button>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Yes but what if I have multiple subscribers to that event. If I use `off()` and a namespace, it will remove *all* of them, not just the one I want? – h bob Oct 12 '14 at 11:24
  • @hbob: You could assign each subscriber a different namespace. Bottom-line, when unsubscribing, you'll need *something* that identifies what it is you want to remove (either the actual function you're attaching, or a specific namespace). – T.J. Crowder Oct 12 '14 at 11:25
  • @TJCrowder: Yes makes sense. I've added my solution as an answer, what do you think of it? I'm accepting yours though for giving me the original idea. – h bob Oct 12 '14 at 12:08
1

You can return the new created event handler like this:

var subscribe = function(name, callback) {
  var handler = function () {
    var args = Array.prototype.slice.call(arguments);
    args = args.slice(1);
    callback.apply(null, args);
  };
  $("html").on(name, handler);
  return handler;
};

Now you can use it like this:

var myhandler = function () { console.log(arguments); };
var eventHandler = subscribe("foo", myhandler);
publish("foo", 1, false, "bob", {a:1, b:"2"}); // logs to console
unsubscribe("foo", eventHandler);
publish("foo", 1, false, "bob", {a:1, b:"2"}); // don't log anymore

Here is the demo

friedi
  • 4,350
  • 1
  • 13
  • 19
0

What I've done is based on the namespaces idea proposed by @TJCrowder. But I've marked his as the answer as thanks.

var subscribe = function(name, callback) {

  // create a unique "key" for this specific event handler
  // - use this format: ORIGINALNAME.MILLISECONDS.RANDOMNUMBER
  // - include time in random string, as could be lots of subs happening at once
  // - then include a random number, just to be safe
  var key = name + "." + (new Date().getTime()) + (Math.random()*1000);

  $("html").on(key, function () {
    var args = Array.prototype.slice.call(arguments);
    args = args.slice(1);
    callback.apply(null, args);
  });

  return key;
}

var unsubscribe = function (key) {
  $("html").off(key);
};

// usage:
var myhandler = function () { /* ... */ };
var key = subscribe("foo", myhandler);
publish("foo", 1, false, "bob", {a:1, b:"2"});
unsubscribe(key);

Since this key is unique (well, it should be?!), it will only remove this particular handler. So I can have many subscribers, and only unsub one of them.

h bob
  • 3,610
  • 3
  • 35
  • 51
  • That seems fine. (Other than: What's with the `Math.random()*(1000-0)+0`? That's exactly the same as `Math.random()*1000`.) Of course, if you're going to remember something unique, you could just return a reference to the function you're creating. You'd want to release the reference when unsubscribing, which is easily done: `key = unsubscribe(key);` Since `unsubscribe` doesn't return anything, calling it results in `undefined`, which gets assigned to `key`, whereupon the function no longer has any references to it and can be garbage-collected. – T.J. Crowder Oct 12 '14 at 12:11
  • @TJCrowder Although you're 100% right, and your way is simpler, I think I prefer to return the unique string as a key. Makes using the function less prone to memory leaks by a dev who forgets to force the garbage collector. – h bob Oct 12 '14 at 12:21
  • Right. Just one copy of the function will exist, the value returned will be a reference to that one function. – T.J. Crowder Oct 12 '14 at 12:21
  • *"Makes using the function less prone to memory leaks by a dev who forgets to force the garbage collector"* Yes and no. If they don't clear `key`, there's this string lying around. But the string is *probably* smaller than the function. Not by much (for the function you've shown), but still... – T.J. Crowder Oct 12 '14 at 12:22
  • @T.J.Crowder Yes I'm assuming the string is way smaller than the callback. The one I used above is just boilerplate. Maybe a compromise is to clear it using your way: `key = unsub(key)`, and fall back on knowing that if he forgets, well, it's just a few hundred/thousand strings in RAM, rather than callbacks. – h bob Oct 12 '14 at 12:23
  • 1
    Thinking about it: It's not just the function, it's the contexts the function closes over, and anything referenced by those contexts, so although the string might not be much smaller than the function, I bet it's a lot smaller than everything the function would keep in RAM. Good call. – T.J. Crowder Oct 12 '14 at 12:30