Skip to content

Commit 4662d2b

Browse files
committed
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 jquery/jquery#4453 Ref jquery/jquery#4454 Ref jquery/jquery#4332 Ref gh-405
1 parent 11767a1 commit 4662d2b

File tree

3 files changed

+68
-10
lines changed

3 files changed

+68
-10
lines changed

src/sizzle.js

+27-10
Original file line numberDiff line numberDiff line change
@@ -329,24 +329,30 @@ function Sizzle( selector, context, results, seed ) {
329329
// Thanks to Andrew Dupont for this technique.
330330
if ( nodeType === 1 && rdescend.test( selector ) ) {
331331

332-
// Capture the context ID, setting it first if necessary
333-
if ( ( nid = context.getAttribute( "id" ) ) ) {
334-
nid = nid.replace( rcssescape, fcssescape );
335-
} else {
336-
context.setAttribute( "id", ( nid = expando ) );
332+
// Expand context for sibling selectors
333+
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
334+
context;
335+
336+
// We can use :scope instead of the ID hack if the browser
337+
// supports it & if we're not changing the context.
338+
if ( newContext !== context || !support.scope ) {
339+
340+
// Capture the context ID, setting it first if necessary
341+
if ( ( nid = context.getAttribute( "id" ) ) ) {
342+
nid = nid.replace( rcssescape, fcssescape );
343+
} else {
344+
context.setAttribute( "id", ( nid = expando ) );
345+
}
337346
}
338347

339348
// Prefix every selector in the list
340349
groups = tokenize( selector );
341350
i = groups.length;
342351
while ( i-- ) {
343-
groups[ i ] = "#" + nid + " " + toSelector( groups[ i ] );
352+
groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " +
353+
toSelector( groups[ i ] );
344354
}
345355
newSelector = groups.join( "," );
346-
347-
// Expand context for sibling selectors
348-
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
349-
context;
350356
}
351357

352358
try {
@@ -626,6 +632,17 @@ setDocument = Sizzle.setDocument = function( node ) {
626632
}
627633
}
628634

635+
// Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only,
636+
// Safari 4 - 5 only, Opera <=11.6 - 12.x only
637+
// IE/Edge & older browsers don't support the :scope pseudo-class.
638+
// Support: Safari 6.0 only
639+
// Safari 6.0 supports :scope but it's an alias of :root there.
640+
support.scope = assert( function( el ) {
641+
docElem.appendChild( el ).appendChild( document.createElement( "div" ) );
642+
return typeof el.querySelectorAll !== "undefined" &&
643+
!el.querySelectorAll( ":scope fieldset div" ).length;
644+
} );
645+
629646
/* Attributes
630647
---------------------------------------------------------------------- */
631648

test/data/fixtures.html

+1
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,6 @@
269269
</em>
270270
<span id="siblingspan"></span>
271271
</div>
272+
272273
<br id="last"/>
273274
</div>

test/unit/selector.js

+40
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,46 @@ QUnit.test( "context", function( assert ) {
13671367
);
13681368
} );
13691369

1370+
( function() {
1371+
var scopeSupport;
1372+
try {
1373+
document.querySelectorAll( ":scope" );
1374+
scopeSupport = true;
1375+
} catch ( e ) {
1376+
scopeSupport = false;
1377+
}
1378+
1379+
// Support: IE 6 - 11+, Edge 12 - 18+, Chrome <=25 only, Safari <=6 only, Firefox <=13 only, Opera <=12 only
1380+
// Older browsers don't support the :scope pseudo-class so they may trigger MutationObservers.
1381+
// The test is skipped there.
1382+
QUnit[ scopeSupport && window.MutationObserver ? "test" : "skip" ](
1383+
"selectors maintaining context don't trigger mutation observers", function( assert ) {
1384+
assert.expect( 1 );
1385+
1386+
var timeout,
1387+
done = assert.async(),
1388+
elem = document.createElement( "div" );
1389+
1390+
elem.innerHTML = "<div></div>";
1391+
1392+
var observer = new MutationObserver( function() {
1393+
clearTimeout( timeout );
1394+
observer.disconnect();
1395+
assert.ok( false, "Mutation observer fired during selection" );
1396+
done();
1397+
} );
1398+
observer.observe( elem, { attributes: true } );
1399+
1400+
Sizzle( "div div", elem );
1401+
1402+
timeout = setTimeout( function() {
1403+
observer.disconnect();
1404+
assert.ok( true, "Mutation observer didn't fire during selection" );
1405+
done();
1406+
} );
1407+
} );
1408+
} )();
1409+
13701410
QUnit.test( "caching", function( assert ) {
13711411
assert.expect( 3 );
13721412

0 commit comments

Comments
 (0)