On jQuery's live()
Since the release of 1.4, there's been a lot of discussion about live(). Now that it's almost as fully featured as bind(), it's getting quite a workout.
One of the larger calls to action is to introduce $.live(selector,eventType,handler). There are two primary reasons I've seen behind this proposal:
- There is a performance hit for the initial selection in
$('a.whatever').live(.. that isn't neccessary live()cannot work on any arbitrary collection of DOM elements in jQuery object, as a side effect of a very different internal operation, thus providing an inconsistent developer experience than all other $.fn.foo methods.
$.live for performance
Sizzle is used to find any matching elements before the live method discards them an only uses the jQobj.selector property and context. It's a waste, I agree. But you can avoid it. Zachleat posted this technique first. Then I stole a code snippet from @janl to show off in my jQuery Performance talk for jQCon. This inspired furf's jQuery.live() patch. And now jmar777 has resurrected that discussion.
I do not think the performance gain in $.live is worth adding it to jQuery core, by any means. Let's take an example.
Let's say we're dealing with
$(selector).live('click',fn)On every click on the page, the event is caught at the context.. the
document. Butevt.targetpoints to the top-most element that was clicked on.
jQuery then traverses from evt.target through each ancestor of that node all the way up to document checkingif (jQuery(elem).is(selector)).[source]I think a reasonable estimated depth of evt.targets is like 8 (From the document to HTML element to BODY and so on.. a depth of 20 is not uncommon by any means). And lets say we have an average 5 clicks on a page.
That's 40 total checks against theselector(plus the original one done at bind time).
So in my opinion a 1/41 speed gain isn't worth changing the API.
Scoping the delegation a context, however has a much larger impact on performance: $(selector,elem).live('click',fn) Here, only clicks within the context are caught, so not only is the parent traversal depth much more shallow, but that entire sequence is avoided for all events occurring outside that area.
The current API to take advantage of this optimization requires code like $('a.awesome', $('#box')[0]).live(... Yuck! So I wrote a patch to deliver this same optimization to any ID-rooted selectors. (The perk being that everyone who learns #id-rooted selectors are fast for selecting get a related perf boost here). But as Justin Meyer pointed out in the comments, it's not as flexible as live() currently is.
$.fn.live cannot operate on any jQuery-wrapped collection of elements
$('a.foo').filter('.bar').live(... // fails $('a.link').parent().nextAll().live(... // fails $(this).live(... // fails
Quite simply, $.fn.live needs to be at the top of a chain to guarantee the .selector and .context properties of the jQuery object represent that set. Unlike other $.fn methods, live utilizes those properties and not the actual collection of elements that result from selection, traversing, and filtering.
$.live() and let $.die()
What are the possible solutions?
- Turn on a dirty flag in traversal methods, so if you try and live() that collection, it will throw an error.
- Deprecate $.fn.live and use $.live(selector[,context],type,handler)
- Use $.live to create a "live jQuery object" that defaults to live-bindings:
$.live(selector,context).click(fn).mouseenter(fn).unbind("click")[source] - Change $.fn.live to operate from a context:
$(context).live('selector', event, fn)This bakes the performance benefit from above right in.
Any others? What do you guys think? Is there a solution that preserves backwards-compatibility while solving the issue?
btw- thx to jmar777, yehuda katz, ben alman, ajpiano, brandon aaron and julian aubourg for their great thoughts on the issue.
Brandon Aaron suggested #4 which I really enjoy. It demystifies context (instead of miraculously defaulting to `document` all the time) and, as mentioned, gains the performance benefit.
I wonder though, is there a way to maintain the current $.fn.live behavior and add this in? It doesn't seem like it to me.. :/
Thanks entertaining the discussion Paul – I've added a link to this post on the forum discussion: http://forum.jquery.com/topic/jquery-live-jquery-fn-live-discussion. To respond, let me approach both issues separately:
Regarding your first point (performance), I must concede that the difference is generally going to be unnoticeable, especially when executed before the DOM ready. Even after DOM ready, there are further ways to reduce the cost, via better selectors and scoping (as you outlined). While one could argue, "why is there ANY hit at all?", I think this issue is secondary, and I don't want to belabor/oversell my point here.
I think the larger issue is the API. All of the $.fn methods operate in a consistent fashion. As you described above, $.fn.live departs from a well established pattern. Needless to say, there are tons of other libraries out there, many of which can handle anything you can throw at jQuery – as @phiggins is fond of saying, "it's just javascript". I think what sets jQuery apart is a stellar API. It simply behaves in a supremely predictable way. $.fn.live, however, breaks several assumptions that are true of (most) every other method in the $.fn namespace. The glories of chaining and blood-simple traversal/manipulation are fantastic features of jQuery – it just seems a shame to have to interject "but watch out for .live!" in such discussions.
Well… I think I've exceeded++ the max length of any self-respecting comment, so I'll wrap up here. I did want to link real fast to Ben Alman's comment, because I feel that it provides a much more succinct response than I've provided: http://forum.jquery.com/topic/jquery-live-jquery-fn-live-discussion#14737000000654148
@Paul Irish
I actually rather like this idea as well. Perhaps a $.fn.delegate method, where it is clear that you are selecting the context?
I chose the particular approach I went with because it preserves $.fn.live (given it's widespread use). It just offers an alternative that makes more sense to me….
I've pretty much been screaming for #4 and trying to get it called delegate months ago.
http://groups.google.com/group/jquery-dev/browse_thread/thread/e3bb36f8a9dae2b5/7e3bc980cd327a74
I'll articulate the reasons for #4 and delegate a little more.
1. The Live plugin was similar, but not the same thing as event delegation. It's confusing. Calling it delegate is much clearer.
2. The 'chaining' factor. $(".foo").delegate(selector, event, func).show() is much more clear where the 'action is' (on foo).
3. Performance. You want people listening as low on the dom as possible. With JMVC, until you get 100s of selector comparisons, walking up the parent list was still the most expensive thing. You are training people correctly.
4. Impossible to build 'plugins with'. $().live is pretty much impossible to make sharable plugins with. As the selector is on the DOM, you might be responding to events not in your widget's control.
People might not like $().delegate … but at least they will have to understand the principles of what is going on.
I can't understand why we would want something that would depart so much from jQuery's conventions. #3 is clearly the way to go.
It's simple, exactly parallelizes jQuery(selector,context), brings all the event related methods into the live landscape, is more flexible, more readable (as in less cumbersome code just to bind a click handler — please no bind method with N arguments, doing simple stuff should be simple), doesn't REQUIRE to ALWAYS specify a context.
In the end, what we bind live events to is nothing but a nodelist which happen to be virtual rather than real (as opposed to classic event binding). Having a function that mimics jQuery(), creates a virtual nodelist (no selection) with properly handled context and provides all the methods we're used to have for "real" nodelists is simple to implement, simple to use, solves all the issues regarding $.fn.live() and is backward compatible.
Maybe we could call it dynamic or whatever rather than live but, whatever, in the end, I think the two lines of code below speak for themselves:
Personally, and I might be alone on this one, but my vote is for #2. The biggest performance hits I see (weighting importance to browser usage stats) are the selectors (usually executed on load), not the event callbacks (executed when users interact with the page). It's really about load script execution time versus responsiveness, is it not?
As for a standard API: #2 still maintains consistency with "the jQuery way," since it's separate altogether from $.fn.
As unpopular as I think it would be, I think the "right" thing to do is solutions 1 and 2 — add the dirty flag, deprecate $.fn.live, and move to $.live. For something on $.fn to not be chainable feels wrong, and it's going to trip up a lot of people. The trouble that would be required to make $.fn.live work as it might be expected to is not worth it — $.live is a more accurate home for the method.
All that said, I realize the extreme pain that would be involved in deprecating $.fn.live so soon after its release, so at the end of the day, what's "right" may not be what's practical. More practical is probably just solution 1 on its own — throw errors on troublesome uses of $.fn.live, and leave the rest alone.
Wow, there's a lot of awesome feedback showing up here. I agree with @Rebecca, in that removing $.fn.live altogether would wreak havoc on the jQuery landscape. It would arguably be a good thing from an "ideological" standpoint, but most likely not a very pragmatic solution. I think that's the main appeal of simply adding $.live, rather than making it a wholesale replacement.
An interesting point that John Resig raised in the forum, that I mostly agree with, is that it is also counter-convention to be passing selectors into a $.foo signature – this has exclusively been the domain of the $.fn.foo methods. This is probably my largest concern with adding $.live. My response is still that I'd rather expand the scope of what $.foo handles, rather than break the $.fn convention (for all the reasons we've been discussing). Whatever the outcome, I am definitely enjoying watching this discussion play out – its fun to see a lot of bright minds throw their attention at this.
Well i haven't really tried a live function, but i know how to handle things in jQuery for the issues for which live was introduced. Althought they might be tedious one, but I got so many websites using older version of jQuery that i dont thing its worth upgrading it since i am not getting paid for it…. still will love to learn pros and cons and this post helped a lot
Here's a thought, could you add support in the selector to create an empty jQuery object with a selector property? That way, something like $('! a'); would create an object with 'a' as the selector, but not select anything. It would be prefect object to pass to $.fn.live, assuming you don't need to chain. To reap the performance benefit for live, you would just need to add the special character to selectors. $('.myAwesomeClass').live(…); becomes $('! .myAwesomeClass').live(…). This would also allow plugins that only really need a selector to have a friendly syntax. $('! a[title]').tooltipLive();
Great post, Paul. Thanks for pulling the threads together and putting the heads together. I am in almost total agreement with everyone. (How is that possible?) My only dispute would be with your "1/41" assumption. I believe that the benefit of non-instantiation is far greater than you estimate, as that one check at bind time is gathering a potentially lengthy collection which can block *all* interaction until complete (if not done properly). But seeing as all of the proffered solutions eliminate the need to perform this check, the point is moot.
1. Implement #1 (dirty flags and errors) immediately to prevent the "// fails". If it's broke and ya can't (or won't) fix it, at least put a sign on it.
2. Implement a proper event delegation method for clarity, ala #4, but make the argument signature more consistent with jQuery's other event methods:
3. For backward compatibility, rewrite $.fn.live() to use the new $.fn.delegate() method.
equals:
4. Consider a name change. Delegate means "to appoint as one's representative". The syntax above implies that the context is appointing selector as representative when the reverse is true. Maybe:
(At least that would make naming the unbind method less awkward!)
However, I maintain one reservation about this course of action, and it's a big one: Readability.
Take for example:
Does this syntax adequately express the developer's intent to handle the click event of anchors having class email? I dare say it doesn't. Instead, it reflects the inner workings of the jQuery library. Are we limited by the library? By its conventions? Or by our imaginations?
Would either of these be any better?
I don't know, maybe, but, at the end of the day, sacrifices and compromises will need to be made to bring improved event delegation to jQuery. If you're going to honor the promise of live() (and $.fn.livequery() before it), that events could be bound "auto-magically, even after the page has been loaded and the DOM updated," then you may have to reject certain conventions about collections and library signatures, pour yourself a cup of tea, and meditate on the one true convention, "Write less, do more."
Friend @deadlyicon has offered this implementation of delegate():
https://gist.github.com/287333/22dea913a4fefcd06640c6391d5be05ea1a47436
Interesting that you see it this way. I find that it eminently readable. I parse it this way:
"Inside comments, delegate clicks on email links to sendEmail."
This is the same argument I made in a Google Groups thread a few months back.
Nice tutorial.. Thanks !
Yah the chaining issue is really a pain. Im throwing my hat in with #3.