Skip to content

Commit f95c2c3

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 Ref jquerygh-4453 Ref jquerygh-4454 Ref jquerygh-4332 Ref jquery/sizzle#405
1 parent ebb312e commit f95c2c3

File tree

4 files changed

+92
-12
lines changed

4 files changed

+92
-12
lines changed

src/selector.js

+18-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ define( [
77
"./var/push",
88
"./selector/support",
99

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

@@ -245,24 +246,30 @@ function find( selector, context, results, seed ) {
245246
// Thanks to Andrew Dupont for this technique.
246247
if ( nodeType === 1 && rdescend.test( selector ) ) {
247248

248-
// Capture the context ID, setting it first if necessary
249-
if ( ( nid = context.getAttribute( "id" ) ) ) {
250-
nid = jQuery.escapeSelector( nid );
251-
} else {
252-
context.setAttribute( "id", ( nid = expando ) );
249+
// Expand context for sibling selectors
250+
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
251+
context;
252+
253+
// We can use :scope instead of the ID hack if the browser
254+
// supports it & if we're not changing the context.
255+
if ( newContext !== context || !support.scope ) {
256+
257+
// Capture the context ID, setting it first if necessary
258+
if ( ( nid = context.getAttribute( "id" ) ) ) {
259+
nid = jQuery.escapeSelector( nid );
260+
} else {
261+
context.setAttribute( "id", ( nid = expando ) );
262+
}
253263
}
254264

255265
// Prefix every selector in the list
256266
groups = tokenize( selector );
257267
i = groups.length;
258268
while ( i-- ) {
259-
groups[ i ] = "#" + nid + " " + toSelector( groups[ i ] );
269+
groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " +
270+
toSelector( groups[ i ] );
260271
}
261272
newSelector = groups.join( "," );
262-
263-
// Expand context for sibling selectors
264-
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
265-
context;
266273
}
267274

268275
try {

src/selector/support.js

+7
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ support.disconnectedMatch = assert( function( el ) {
4949
return matches.call( el, "*" );
5050
} );
5151

52+
// Support: IE 9 - 11+, Edge 12 - 18+
53+
// IE/Edge don't support the :scope pseudo-class.
54+
try {
55+
document.querySelectorAll( ":scope" );
56+
support.scope = true;
57+
} catch ( e ) {}
58+
5259
return support;
5360

5461
} );

test/unit/selector.js

+35
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,41 @@ QUnit.test( "context", function( assert ) {
16401640
}
16411641
} );
16421642

1643+
// Support: IE 11+, Edge 12 - 18+
1644+
// IE/Edge don't support the :scope pseudo-class so they will trigger MutationObservers.
1645+
// The test is skipped there.
1646+
QUnit[
1647+
( QUnit.isIE || /edge\//i.test( navigator.userAgent ) ) ?
1648+
"skip" :
1649+
"test"
1650+
]( "selectors maintaining context don't trigger mutation observers", function( assert ) {
1651+
assert.expect( 1 );
1652+
1653+
var timeout,
1654+
done = assert.async(),
1655+
container = jQuery( "<div/>" ),
1656+
child = jQuery( "<div/>" );
1657+
1658+
child.appendTo( container );
1659+
container.appendTo( "#qunit-fixture" );
1660+
1661+
var observer = new MutationObserver( function() {
1662+
clearTimeout( timeout );
1663+
observer.disconnect();
1664+
assert.ok( false, "Mutation observer fired during selection" );
1665+
done();
1666+
} );
1667+
observer.observe( container[ 0 ], { attributes: true } );
1668+
1669+
container.find( "div div" );
1670+
1671+
timeout = setTimeout( function() {
1672+
observer.disconnect();
1673+
assert.ok( true, "Mutation observer didn't fire during selection" );
1674+
done();
1675+
} );
1676+
} );
1677+
16431678
QUnit.test( "caching does not introduce bugs", function( assert ) {
16441679
assert.expect( 3 );
16451680

test/unit/support.js

+32-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ testIframe(
7777
radioValue: true,
7878
reliableMarginLeft: true,
7979
reliableTrDimensions: false,
80+
scope: false,
8081
scrollboxSize: true
8182
},
8283
ie_10_11: {
@@ -98,6 +99,7 @@ testIframe(
9899
radioValue: false,
99100
reliableMarginLeft: true,
100101
reliableTrDimensions: false,
102+
scope: false,
101103
scrollboxSize: true
102104
},
103105
ie_9: {
@@ -119,6 +121,7 @@ testIframe(
119121
radioValue: false,
120122
reliableMarginLeft: true,
121123
reliableTrDimensions: false,
124+
scope: false,
122125
scrollboxSize: false
123126
},
124127
chrome: {
@@ -140,6 +143,7 @@ testIframe(
140143
radioValue: true,
141144
reliableMarginLeft: true,
142145
reliableTrDimensions: true,
146+
scope: true,
143147
scrollboxSize: true
144148
},
145149
safari: {
@@ -161,6 +165,7 @@ testIframe(
161165
radioValue: true,
162166
reliableMarginLeft: true,
163167
reliableTrDimensions: true,
168+
scope: true,
164169
scrollboxSize: true
165170
},
166171
safari_9_10: {
@@ -182,6 +187,7 @@ testIframe(
182187
radioValue: true,
183188
reliableMarginLeft: true,
184189
reliableTrDimensions: true,
190+
scope: true,
185191
scrollboxSize: true
186192
},
187193
firefox: {
@@ -203,6 +209,7 @@ testIframe(
203209
radioValue: true,
204210
reliableMarginLeft: true,
205211
reliableTrDimensions: false,
212+
scope: true,
206213
scrollboxSize: true
207214
},
208215
firefox_60: {
@@ -224,6 +231,7 @@ testIframe(
224231
radioValue: true,
225232
reliableMarginLeft: false,
226233
reliableTrDimensions: true,
234+
scope: true,
227235
scrollboxSize: true
228236
},
229237
ios: {
@@ -245,6 +253,7 @@ testIframe(
245253
radioValue: true,
246254
reliableMarginLeft: true,
247255
reliableTrDimensions: true,
256+
scope: true,
248257
scrollboxSize: true
249258
},
250259
ios_9_10: {
@@ -266,6 +275,7 @@ testIframe(
266275
radioValue: true,
267276
reliableMarginLeft: true,
268277
reliableTrDimensions: true,
278+
scope: true,
269279
scrollboxSize: true
270280
},
271281
ios_8: {
@@ -287,6 +297,7 @@ testIframe(
287297
radioValue: true,
288298
reliableMarginLeft: true,
289299
reliableTrDimensions: true,
300+
scope: true,
290301
scrollboxSize: true
291302
},
292303
ios_7: {
@@ -308,6 +319,7 @@ testIframe(
308319
radioValue: true,
309320
reliableMarginLeft: true,
310321
reliableTrDimensions: true,
322+
scope: true,
311323
scrollboxSize: true
312324
},
313325
android: {
@@ -329,6 +341,7 @@ testIframe(
329341
radioValue: true,
330342
reliableMarginLeft: false,
331343
reliableTrDimensions: true,
344+
scope: false,
332345
scrollboxSize: true
333346
}
334347
};
@@ -385,6 +398,15 @@ testIframe(
385398
j++;
386399
}
387400

401+
// Add an assertion per undefined support prop as it may
402+
// not even exist on computedSupport but we still want to run
403+
// the check.
404+
for ( prop in expected ) {
405+
if ( expected[ prop ] === undefined ) {
406+
j++;
407+
}
408+
}
409+
388410
assert.expect( j );
389411

390412
for ( i in expected ) {
@@ -413,14 +435,23 @@ testIframe(
413435
i++;
414436
}
415437

438+
// Add an assertion per undefined support prop as it may
439+
// not even exist on computedSupport but we still want to run
440+
// the check.
441+
for ( prop in expected ) {
442+
if ( expected[ prop ] === undefined ) {
443+
i++;
444+
}
445+
}
446+
416447
assert.expect( i );
417448

418449
// Record all support props and the failing ones and ensure everyone
419450
// except a few on a whitelist are failing at least once.
420451
for ( browserKey in expectedMap ) {
421452
for ( supportTestName in expectedMap[ browserKey ] ) {
422453
supportProps[ supportTestName ] = true;
423-
if ( expectedMap[ browserKey ][ supportTestName ] !== true ) {
454+
if ( !expectedMap[ browserKey ][ supportTestName ] ) {
424455
failingSupportProps[ supportTestName ] = true;
425456
}
426457
}

0 commit comments

Comments
 (0)