Skip to content

Commit 0b62852

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 jquerygh-4453 Ref jquerygh-4332 Ref jquery/sizzle#405
1 parent b334ce7 commit 0b62852

File tree

3 files changed

+73
-18
lines changed

3 files changed

+73
-18
lines changed

src/selector.js

+19-11
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ define( [
44
"./var/indexOf",
55
"./var/pop",
66
"./var/push",
7+
"./selector/support",
78

89
// The following utils are attached directly to the jQuery object.
910
"./selector/contains",
1011
"./selector/escapeSelector",
1112
"./selector/uniqueSort"
12-
], function( jQuery, document, indexOf, pop, push ) {
13+
], function( jQuery, document, indexOf, pop, push, support ) {
1314

1415
"use strict";
1516

@@ -153,7 +154,7 @@ function selectorError( msg ) {
153154
}
154155

155156
function find( selector, context, results, seed ) {
156-
var m, i, elem, nid, match, groups, newSelector,
157+
var m, i, elem, nid, match, groups, newSelector, canUseScope,
157158
newContext = context && context.ownerDocument,
158159

159160
// nodeType defaults to 9, since context defaults to document
@@ -230,24 +231,31 @@ function find( selector, context, results, seed ) {
230231
// Thanks to Andrew Dupont for this technique.
231232
if ( nodeType === 1 && rdescend.test( selector ) ) {
232233

234+
// Expand context for sibling selectors
235+
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
236+
context;
237+
238+
// We can use :scope instead of the ID hack if the browser
239+
// supports it & if we're not changing the context.
240+
canUseScope = newContext === context && support.scope;
241+
233242
// Capture the context ID, setting it first if necessary
234-
if ( ( nid = context.getAttribute( "id" ) ) ) {
235-
nid = jQuery.escapeSelector( nid );
236-
} else {
237-
context.setAttribute( "id", ( nid = expando ) );
243+
if ( !canUseScope ) {
244+
if ( ( nid = context.getAttribute( "id" ) ) ) {
245+
nid = jQuery.escapeSelector( nid );
246+
} else {
247+
context.setAttribute( "id", ( nid = expando ) );
248+
}
238249
}
239250

240251
// Prefix every selector in the list
241252
groups = tokenize( selector );
242253
i = groups.length;
243254
while ( i-- ) {
244-
groups[ i ] = "#" + nid + " " + toSelector( groups[ i ] );
255+
groups[ i ] = ( canUseScope || "#" + nid ) + " " +
256+
toSelector( groups[ i ] );
245257
}
246258
newSelector = groups.join( "," );
247-
248-
// Expand context for sibling selectors
249-
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
250-
context;
251259
}
252260

253261
try {

src/selector/support.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
define( [
2+
"../var/document",
3+
"../var/support"
4+
], function( document, support ) {
5+
6+
"use strict";
7+
8+
// Support: IE 9 - 11+, Edge 12 - 18+
9+
// IE/Edge don't support the :scope pseudo-class.
10+
try {
11+
document.querySelectorAll( ":scope" );
12+
support.scope = ":scope";
13+
} catch ( e ) {}
14+
15+
return support;
16+
17+
} );

test/unit/support.js

+37-7
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,24 @@ testIframe(
5858
var expected,
5959
userAgent = window.navigator.userAgent,
6060
expectedMap = {
61-
edge: {},
62-
ie_11: {},
63-
chrome: {},
64-
safari: {},
65-
firefox: {},
66-
ios: {}
61+
edge: {
62+
scope: undefined
63+
},
64+
ie_11: {
65+
scope: undefined
66+
},
67+
chrome: {
68+
scope: ":scope"
69+
},
70+
safari: {
71+
scope: ":scope"
72+
},
73+
firefox: {
74+
scope: ":scope"
75+
},
76+
ios: {
77+
scope: ":scope"
78+
}
6779
};
6880

6981
if ( /edge\//i.test( userAgent ) ) {
@@ -95,6 +107,15 @@ testIframe(
95107
j++;
96108
}
97109

110+
// Add an assertion per undefined support prop as it may
111+
// not even exist on computedSupport but we still want to run
112+
// the check.
113+
for ( prop in expected ) {
114+
if ( expected[ prop ] === undefined ) {
115+
j++;
116+
}
117+
}
118+
98119
assert.expect( j );
99120

100121
for ( i in expected ) {
@@ -116,14 +137,23 @@ testIframe(
116137
i++;
117138
}
118139

140+
// Add an assertion per undefined support prop as it may
141+
// not even exist on computedSupport but we still want to run
142+
// the check.
143+
for ( prop in expected ) {
144+
if ( expected[ prop ] === undefined ) {
145+
i++;
146+
}
147+
}
148+
119149
assert.expect( i );
120150

121151
// Record all support props and the failing ones and ensure every test
122152
// is failing at least once.
123153
for ( browserKey in expectedMap ) {
124154
for ( supportTestName in expectedMap[ browserKey ] ) {
125155
supportProps[ supportTestName ] = true;
126-
if ( expectedMap[ browserKey ][ supportTestName ] !== true ) {
156+
if ( !expectedMap[ browserKey ][ supportTestName ] ) {
127157
failingSupportProps[ supportTestName ] = true;
128158
}
129159
}

0 commit comments

Comments
 (0)