Skip to content

Commit

Permalink
Selector: Leverage the :scope pseudo-class where possible
Browse files Browse the repository at this point in the history
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

Ref jquerygh-4453
Ref jquerygh-4454
Ref jquerygh-4332
Ref jquery/sizzle#405
  • Loading branch information
mgol committed Sep 19, 2022
1 parent 7cd75da commit e300235
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 12 deletions.
29 changes: 18 additions & 11 deletions src/selector.js
Expand Up @@ -7,7 +7,8 @@ define( [
"./var/push",
"./selector/support",

"./selector/contains", // jQuery.contains
// The following utils are attached directly to the jQuery object.
"./selector/contains",
"./selector/escapeSelector"
], function( jQuery, document, indexOf, hasOwn, pop, push, support ) {

Expand Down Expand Up @@ -245,24 +246,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 {
Expand Down
7 changes: 7 additions & 0 deletions src/selector/support.js
Expand Up @@ -49,6 +49,13 @@ support.disconnectedMatch = assert( function( el ) {
return matches.call( el, "*" );
} );

// 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;

} );
35 changes: 35 additions & 0 deletions test/unit/selector.js
Expand Up @@ -1640,6 +1640,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( "<div/>" ),
child = jQuery( "<div/>" );

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 );

Expand Down
33 changes: 32 additions & 1 deletion test/unit/support.js
Expand Up @@ -77,6 +77,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: false,
scope: false,
scrollboxSize: true
},
ie_10_11: {
Expand All @@ -98,6 +99,7 @@ testIframe(
radioValue: false,
reliableMarginLeft: true,
reliableTrDimensions: false,
scope: false,
scrollboxSize: true
},
ie_9: {
Expand All @@ -119,6 +121,7 @@ testIframe(
radioValue: false,
reliableMarginLeft: true,
reliableTrDimensions: false,
scope: false,
scrollboxSize: false
},
chrome: {
Expand All @@ -140,6 +143,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
safari: {
Expand All @@ -161,6 +165,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
safari_9_10: {
Expand All @@ -182,6 +187,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
firefox: {
Expand All @@ -203,6 +209,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: false,
scope: true,
scrollboxSize: true
},
firefox_60: {
Expand All @@ -224,6 +231,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: false,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
ios: {
Expand All @@ -245,6 +253,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
ios_9_10: {
Expand All @@ -266,6 +275,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
ios_8: {
Expand All @@ -287,6 +297,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
ios_7: {
Expand All @@ -308,6 +319,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: true,
reliableTrDimensions: true,
scope: true,
scrollboxSize: true
},
android: {
Expand All @@ -329,6 +341,7 @@ testIframe(
radioValue: true,
reliableMarginLeft: false,
reliableTrDimensions: true,
scope: false,
scrollboxSize: true
}
};
Expand Down Expand Up @@ -385,6 +398,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 ) {
Expand Down Expand Up @@ -413,14 +435,23 @@ 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 everyone
// except a few on a whitelist are failing at least once.
for ( browserKey in expectedMap ) {
for ( supportTestName in expectedMap[ browserKey ] ) {
supportProps[ supportTestName ] = true;
if ( expectedMap[ browserKey ][ supportTestName ] !== true ) {
if ( !expectedMap[ browserKey ][ supportTestName ] ) {
failingSupportProps[ supportTestName ] = true;
}
}
Expand Down

0 comments on commit e300235

Please sign in to comment.