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
This commit is contained in:
Michał Gołębiowski-Owczarek 2019-08-19 18:41:03 +02:00 committed by GitHub
parent 7bdf307b51
commit df6a7f7f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 107 additions and 18 deletions

View File

@ -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 ) ) {
// 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 {

17
src/selector/support.js Normal file
View File

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

View File

@ -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( "<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 );

View File

@ -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;
}
}