简体   繁体   中英

Multiple searching of the same elements in DOM

There are perfomanse problems with bubbling subscribing via jQuery. IE9 and IE11 tell that 80% of time are spent on executing querySelectorAll . Analisis shows the function $.event.dispatch (in jQuery 1.8.1, in newer version (1.11.3) this functionality was moved to $.event.handlers ), which containes following code:

for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {

  // Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764)
  if ( cur.disabled !== true || event.type !== "click" ) {
    selMatch = {};
    matches = [];
    for ( i = 0; i < delegateCount; i++ ) {
      handleObj = handlers[ i ];
      sel = handleObj.selector;

      if ( selMatch[ sel ] === undefined ) {
        selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0;
      }
      if ( selMatch[ sel ] ) {
        matches.push( handleObj );
      }
    }
    if ( matches.length ) {
      handlerQueue.push({ elem: cur, matches: matches });
    }
  }
}

Attension to following lines:

// For each element from clicked and above
for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
    // Clear the search cache
    selMatch = {};

    // For each subscriber
    for ( i = 0; i < delegateCount; i++ ) {
      // Take the subscriber's selector
      sel = handleObj.selector;

      // If it's out of cache
      if ( selMatch[ sel ] === undefined ) {
        // Search for elements matching to the selector
        // And remember if current element is among of found ones
        selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0;

As the subscription is made on body, every such search takes all elements matching the selector from the whole document. And this is repeated as many times as the depth of the clicked element.

As I understand, the outer loop is to garantee write order of handlers according to bubbling order. There is a cache, but it works only at one level and helps in case of multiple subscription with the same selector.

The question is why cache is implemented in such manner? Why not to keep the jQuery collection and move index to the next if condition?


But that's not all. I looked the actual implementation in 1.11.3. It also uses multiple seaches but this line of code is changed.

In 1.8.1 it was:

selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0;

In 1.11.3 it have become:

matches[ sel ] = handleObj.needsContext ?
jQuery( sel, this ).index( cur ) >= 0 :
jQuery.find( sel, this, null, [ cur ] ).length;

Same modification in this case doesn't seem to be reasonable.


So, the questions are:

  1. What reason could've lead to this not optimal code?
  2. What should I do to solve perfomance problems?

The following snippet shows the problem.

If you open the browser console and click Click me to get a lot of searches! . you'll see following lines outputted 21 times:

qsa [id='sizcache041783330822363496'] section .smth
gbc smth-other

About the segment [id='sizcache041783330822363496'] there is a related question in Russian . Shortly, such form simplifies escaping of special characters in id if it was original. Some time ago Sizzle has updated this place , but even actual jQuery version doesn't contain it.

 $(function () { $("body") .on("click", "section .smth", function () { console.log("clicked", "section .smth") }) .on("click", ".smth-other", function () { console.log("clicked", ".smth-other") }); $("h1").text("Click me to get a lot of searches!"); var qsa = Element.prototype.querySelectorAll, gbc = Element.prototype.getElementsByClassName; Element.prototype.querySelectorAll = function(s) { console.log('qsa', s); return qsa.apply(this, arguments) }; Element.prototype.getElementsByClassName = function(s) { console.log('gbc', s); return gbc.apply(this, arguments) }; }); 
 body { counter-reset: lev 1; } div { counter-increment: lev; } h1, h2 { cursor: pointer; } h1:hover, h2:hover { background: silver; } h1:after { content: " (" counter(lev) ")"; } 
 <div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><h1> Loading... </h1></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div> <section> <h2 class="smth">I'm smth and i'm waiting for a click</h2> </section> <section> <h2 class="smth-other">I'm smth other and i'm waiting for a click</h2> </section> <script src="//code.jquery.com/jquery-1.8.1.js"></script> 


The next snippet shows the full code of jQuery 1.8.1 dispatch function (snippet is used to make collapsible spoiler, it's not for running the code):

 dispatch: function( event ) { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event || window.event ); var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related, handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), delegateCount = handlers.delegateCount, args = [].slice.call( arguments ), run_all = !event.exclusive && !event.namespace, special = jQuery.event.special[ event.type ] || {}, handlerQueue = []; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[0] = event; event.delegateTarget = this; // Call the preDispatch hook for the mapped type, and let it bail if desired if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } // Determine handlers that should run if there are delegated events // Avoid non-left-click bubbling in Firefox (#3861) if ( delegateCount && !(event.button && event.type === "click") ) { for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { // Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764) if ( cur.disabled !== true || event.type !== "click" ) { selMatch = {}; matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; sel = handleObj.selector; if ( selMatch[ sel ] === undefined ) { selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0; } if ( selMatch[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push({ elem: cur, matches: matches }); } } } } // Add the remaining (directly-bound) handlers if ( handlers.length > delegateCount ) { handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); } // Run delegates first; they may want to stop propagation beneath us for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { matched = handlerQueue[ i ]; event.currentTarget = matched.elem; for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { handleObj = matched.matches[ j ]; // Triggered event must either 1) be non-exclusive and have no namespace, or // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { event.data = handleObj.data; event.handleObj = handleObj; ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { event.result = ret; if ( ret === false ) { event.preventDefault(); event.stopPropagation(); } } } } } // Call the postDispatch hook for the mapped type if ( special.postDispatch ) { special.postDispatch.call( this, event ); } return event.result; }, 

PS: Same question in Russian.

I lifted the line with caching:

if ( delegateCount && !(event.button && event.type === "click") ) {
    selMatch = {};

and moved call to index from cahing to the next condition:

if ( selMatch[ sel ] === undefined ) {
    selMatch[ sel ] = jQuery( sel, this );
}
if ( selMatch[ sel ].index( cur ) >= 0 ) {
    matches.push( handleObj );
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM