From df6a7f7f0f615149266b1a51064293b748b29900 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Mon, 19 Aug 2019 18:41:03 +0200 Subject: [PATCH] Selector: Leverage the :scope pseudo-class where possible The `:scope` pseudo-class[1] has surprisingly good browser support: Chrome, Firefox & Safari have supported if for a long time; only IE & Edge lack support. This commit leverages this pseudo-class to get rid of the ID hack in most cases. Adding a temporary ID may cause layout thrashing which was reported a few times in [the past. We can't completely eliminate the ID hack in modern browses as sibling selectors require us to change context to the parent and then `:scope` stops applying to what we'd like. But it'd still improve performance in the vast majority of cases. [1] https://developer.mozilla.org/en-US/docs/Web/CSS/:scope Fixes gh-4453 Closes gh-4454 Ref gh-4332 Ref jquery/sizzle#405 --- src/selector.js | 29 ++++++++++++++++----------- src/selector/support.js | 17 ++++++++++++++++ test/unit/selector.js | 35 ++++++++++++++++++++++++++++++++ test/unit/support.js | 44 ++++++++++++++++++++++++++++++++++------- 4 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 src/selector/support.js diff --git a/src/selector.js b/src/selector.js index 913f7486f..3e187a159 100644 --- a/src/selector.js +++ b/src/selector.js @@ -4,12 +4,13 @@ define( [ "./var/indexOf", "./var/pop", "./var/push", + "./selector/support", // The following utils are attached directly to the jQuery object. "./selector/contains", "./selector/escapeSelector", "./selector/uniqueSort" -], function( jQuery, document, indexOf, pop, push ) { +], function( jQuery, document, indexOf, pop, push, support ) { "use strict"; @@ -230,24 +231,30 @@ function find( selector, context, results, seed ) { // Thanks to Andrew Dupont for this technique. if ( nodeType === 1 && rdescend.test( selector ) ) { - // Capture the context ID, setting it first if necessary - if ( ( nid = context.getAttribute( "id" ) ) ) { - nid = jQuery.escapeSelector( nid ); - } else { - context.setAttribute( "id", ( nid = expando ) ); + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = jQuery.escapeSelector( nid ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } } // Prefix every selector in the list groups = tokenize( selector ); i = groups.length; while ( i-- ) { - groups[ i ] = "#" + nid + " " + toSelector( groups[ i ] ); + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); } newSelector = groups.join( "," ); - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || - context; } try { diff --git a/src/selector/support.js b/src/selector/support.js new file mode 100644 index 000000000..86cd2d9ae --- /dev/null +++ b/src/selector/support.js @@ -0,0 +1,17 @@ +define( [ + "../var/document", + "../var/support" +], function( document, support ) { + +"use strict"; + +// Support: IE 9 - 11+, Edge 12 - 18+ +// IE/Edge don't support the :scope pseudo-class. +try { + document.querySelectorAll( ":scope" ); + support.scope = true; +} catch ( e ) {} + +return support; + +} ); diff --git a/test/unit/selector.js b/test/unit/selector.js index d6bd62c73..2c17b8682 100644 --- a/test/unit/selector.js +++ b/test/unit/selector.js @@ -1631,6 +1631,41 @@ QUnit.test( "context", function( assert ) { } } ); +// Support: IE 11+, Edge 12 - 18+ +// IE/Edge don't support the :scope pseudo-class so they will trigger MutationObservers. +// The test is skipped there. +QUnit[ + ( QUnit.isIE || /edge\//i.test( navigator.userAgent ) ) ? + "skip" : + "test" + ]( "selectors maintaining context don't trigger mutation observers", function( assert ) { + assert.expect( 1 ); + + var timeout, + done = assert.async(), + container = jQuery( "
" ), + child = jQuery( "
" ); + + child.appendTo( container ); + container.appendTo( "#qunit-fixture" ); + + var observer = new MutationObserver( function() { + clearTimeout( timeout ); + observer.disconnect(); + assert.ok( false, "Mutation observer fired during selection" ); + done(); + } ); + observer.observe( container[ 0 ], { attributes: true } ); + + container.find( "div div" ); + + timeout = setTimeout( function() { + observer.disconnect(); + assert.ok( true, "Mutation observer didn't fire during selection" ); + done(); + } ); +} ); + QUnit.test( "caching does not introduce bugs", function( assert ) { assert.expect( 3 ); diff --git a/test/unit/support.js b/test/unit/support.js index 266b02dd8..8be82bbe9 100644 --- a/test/unit/support.js +++ b/test/unit/support.js @@ -58,12 +58,24 @@ testIframe( var expected, userAgent = window.navigator.userAgent, expectedMap = { - edge: {}, - ie_11: {}, - chrome: {}, - safari: {}, - firefox: {}, - ios: {} + edge: { + scope: undefined + }, + ie_11: { + scope: undefined + }, + chrome: { + scope: true + }, + safari: { + scope: true + }, + firefox: { + scope: true + }, + ios: { + scope: true + } }; if ( /edge\//i.test( userAgent ) ) { @@ -95,6 +107,15 @@ testIframe( j++; } + // Add an assertion per undefined support prop as it may + // not even exist on computedSupport but we still want to run + // the check. + for ( prop in expected ) { + if ( expected[ prop ] === undefined ) { + j++; + } + } + assert.expect( j ); for ( i in expected ) { @@ -116,6 +137,15 @@ testIframe( i++; } + // Add an assertion per undefined support prop as it may + // not even exist on computedSupport but we still want to run + // the check. + for ( prop in expected ) { + if ( expected[ prop ] === undefined ) { + i++; + } + } + assert.expect( i ); // Record all support props and the failing ones and ensure every test @@ -123,7 +153,7 @@ testIframe( for ( browserKey in expectedMap ) { for ( supportTestName in expectedMap[ browserKey ] ) { supportProps[ supportTestName ] = true; - if ( expectedMap[ browserKey ][ supportTestName ] !== true ) { + if ( !expectedMap[ browserKey ][ supportTestName ] ) { failingSupportProps[ supportTestName ] = true; } } -- 2.39.5