jQuery.bind() Data

If you only ever use the type-specific event helpers (.click(), .load(), .change(), etc.), you're potentially missing out on a really handy feature of jQuery: bind data.  Bind data is data associated with the bound handler function, available to every invocation of it, but not in any "normal" variable scope.  It's kind of like currying, except with an event attribute instead of arguments.  You declare bind data like this (as the optional middle parameter of the .bind() method):

jQuery("#selector").bind("click", {name: "barney"}, clickHandler);

And then you use it like this:

function clickHandler(e) {
  alert(e.data.name);
}

The 'e' variable is the standard jQuery event object passed to all event handler functions.  The 'data' key within it contains the bind data created when the handler function was bound, if any.  Even this simple case is potentially interesting because you're controlling the alert message at bind-time, not in the function itself, without the extra wrapper function you'd otherwise use:

jQuery("#selector").click(function() { clickHandler("barney")});
function clickHandler(name) {
  alert(name);
}

But that's not the really useful bit.  What you might not have noticed about the two examples is that in the first, the string "barney" is evaluated once (at bind time), but in the second it's evaluated every time the click event is dispatched.  No big deal for a string, but for complex expressions that can create a performance issue.  Since event handlers are typically executed more than once (and often a lot more than once), moving stuff out of the callback and into bind data can actually yield a noticeable difference in UI performance, as I learned this evening.

In a photo viewing app I built I'm using JS to display multiple photos sequentially via a single IMG tag, just swapping out the source, width/height, title, etc.  I wanted to center the photo tag, but couldn't do it with CSS because some dynamically-sized absolute-positioned elements needed to be accounted for, as well as the size of the photo itself.  I'd originally written a handler that looked something like this:

jQuery("#theImage").load(function() {
  var base = getOffsets("content");
  var total = jQuery("#floatyControlBar").position().left;
  var object = jQuery("#imageContainer");
  var xd = Math.max(0, (total - object.width()) / 2)
  object.css({
    left: base.x + xd + "px",
    top: base.y + "px"
  });
});

It worked great, except that it made a noticeable jump from the old position to the new position when the image loaded.  I first tried the Flash-style "add an animation to mask the slowness", but the browser couldn't handle sliding a big image around very gracefully, so it wasn't any better.  Enter bind data.  Changing the handler to this resolved the issue:

jQuery("#theImage").bind("load", {
  base: getOffsets("content"),
  total: jQuery("#floatyControlBar").position().left,
  object: jQuery("#imageContainer")
}, function(e) {
  var xd = Math.max(0, (e.data.total - e.data.object.width()) / 2);
  e.data.object.css({
    left: e.data.base.x + xd + "px",
    top: e.data.base.y + "px"
  });
});

Basically I just changed it so most of the data collection happens at bind time (so it only happens once) and as little as possible happens when the handler is actually called.  It's a simple optimization, but it eliminated the visible jump.  The code has paid a slight price in readability, no question there, but while we all tout readable code as a hallmark of good code, user experience is still what software is about, so the latter wins.

Of course, you could achieve the same effect by setting the base, total, and object variables into the scope containing the .bind() call so the handler can access them via closure-nature.  But that pollutes (breaks encapsulation), so using bind data is a better solution in most cases.

6 responses to “jQuery.bind() Data”

  1. Robert

    Thanks for the insight.

  2. Tomas

    Thanks a lot! You taught me something new and possibly very useful in my near programming future.

  3. Jozero

    Wow. Thanks ! Stumbling upon your site help solved something I've been stuck on for a while. If you are binding an event dynamically in a loop to serial DOM elements, you can't just access the index counter within each bind event and get the right index, all it does is get the last index. However your binding data to that bind event solved that issue.

    For example if you do this :

    $(".m"+count).bind('dragstop',  function(ev, ui, counter) {
      console.log (counter);
    }
    

    Each time you drag an item, say .m1 or .m2, counter will only return the last value counter was – so its impossible to tie values back to what you are dragging.

    If you do this though thanks to your page :

    $(".m"+count).bind('dragstop', {counter : count},  function(ev, ui) {
     console.log (ev.data.counter);
    }
    

    It has the correct value. That is if you drag around .m1 it returns 1, .m2 returns 2, etc.

  4. Mamoon ur Rasheed

    Thanks man, i was looking for this and it solved my problem

  5. Thomas Kahn

    Thanks! This was a very useful article!

  6. Rali Madhu

    It will only be faster for a single bind. If you do multiple delegate will be faster.