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

Fixes jquerygh-4453
Ref jquerygh-4332
Ref jquery/sizzle#405
  • Loading branch information
mgol committed Aug 12, 2019
1 parent b334ce7 commit 0b62852
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 18 deletions.
30 changes: 19 additions & 11 deletions src/selector.js
Expand Up @@ -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";

Expand Down Expand Up @@ -153,7 +154,7 @@ function selectorError( msg ) {
}

function find( selector, context, results, seed ) {
var m, i, elem, nid, match, groups, newSelector,
var m, i, elem, nid, match, groups, newSelector, canUseScope,
newContext = context && context.ownerDocument,

// nodeType defaults to 9, since context defaults to document
Expand Down Expand Up @@ -230,24 +231,31 @@ 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.
canUseScope = 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 ) );
if ( !canUseScope ) {
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 ] = ( canUseScope || "#" + nid ) + " " +
toSelector( groups[ i ] );
}
newSelector = groups.join( "," );

// Expand context for sibling selectors
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
context;
}

try {
Expand Down
17 changes: 17 additions & 0 deletions 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 = ":scope";
} catch ( e ) {}

return support;

} );
44 changes: 37 additions & 7 deletions test/unit/support.js
Expand Up @@ -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: ":scope"
},
safari: {
scope: ":scope"
},
firefox: {
scope: ":scope"
},
ios: {
scope: ":scope"
}
};

if ( /edge\//i.test( userAgent ) ) {
Expand Down Expand Up @@ -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 ) {
Expand All @@ -116,14 +137,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 every test
// is 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 0b62852

Please sign in to comment.