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 jquery/jquery#4453
Ref jquery/jquery#4454
Ref jquery/jquery#4332
Ref jquerygh-405
  • Loading branch information
mgol committed Aug 19, 2019
1 parent 5d90d4f commit 021a452
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 10 deletions.
36 changes: 26 additions & 10 deletions src/sizzle.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,24 +329,30 @@ function Sizzle( 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 = nid.replace( rcssescape, fcssescape );
} 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 = nid.replace( rcssescape, fcssescape );
} 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 Expand Up @@ -626,6 +632,16 @@ setDocument = Sizzle.setDocument = function( node ) {
}
}

// Support: IE 9 - 11+, Edge 12 - 18+
// IE/Edge don't support the :scope pseudo-class.
// Support: Safari 6.0 only
// Safari 6.0 supports :scope but it's an alias of :root there.
support.scope = assert( function( el ) {
docElem.appendChild( el ).appendChild( document.createElement( "div" ) );
return typeof el.querySelectorAll !== "undefined" &&
!el.querySelectorAll( ":scope fieldset div" ).length;
} );

/* Attributes
---------------------------------------------------------------------- */

Expand Down
1 change: 1 addition & 0 deletions test/data/fixtures.html
Original file line number Diff line number Diff line change
Expand Up @@ -269,5 +269,6 @@
</em>
<span id="siblingspan"></span>
</div>

<br id="last"/>
</div>
40 changes: 40 additions & 0 deletions test/unit/selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,46 @@ QUnit.test( "context", function( assert ) {
);
} );

( function() {
var scopeSupport;
try {
document.querySelectorAll( ":scope" );
scopeSupport = true;
} catch ( e ) {
scopeSupport = false;
}

// Support: IE 6 - 11+, Edge 12 - 18+, Chrome <=25 only, Safari <=6 only, Firefox <=13 only, Opera <=12 only
// Older browsers don't support the :scope pseudo-class so they may trigger MutationObservers.
// The test is skipped there.
QUnit[ scopeSupport && window.MutationObserver ? "test" : "skip" ](
"selectors maintaining context don't trigger mutation observers", function( assert ) {
assert.expect( 1 );

var timeout,
done = assert.async(),
elem = document.createElement( "div" );

elem.innerHTML = "<div></div>";

var observer = new MutationObserver( function() {
clearTimeout( timeout );
observer.disconnect();
assert.ok( false, "Mutation observer fired during selection" );
done();
} );
observer.observe( elem, { attributes: true } );

Sizzle( "div div", elem );

timeout = setTimeout( function() {
observer.disconnect();
assert.ok( true, "Mutation observer didn't fire during selection" );
done();
} );
} );
} )();

QUnit.test( "caching", function( assert ) {
assert.expect( 3 );

Expand Down

0 comments on commit 021a452

Please sign in to comment.