Skip to content

Commit df6a7f7

Browse files
authored
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 gh-4453 Closes gh-4454 Ref gh-4332 Ref jquery/sizzle#405
1 parent 7bdf307 commit df6a7f7

File tree

4 files changed

+107
-18
lines changed

4 files changed

+107
-18
lines changed

src/selector.js

+18-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

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

233-
// 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 ) );
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+
if ( newContext !== context || !support.scope ) {
241+
242+
// Capture the context ID, setting it first if necessary
243+
if ( ( nid = context.getAttribute( "id" ) ) ) {
244+
nid = jQuery.escapeSelector( nid );
245+
} else {
246+
context.setAttribute( "id", ( nid = expando ) );
247+
}
238248
}
239249

240250
// Prefix every selector in the list
241251
groups = tokenize( selector );
242252
i = groups.length;
243253
while ( i-- ) {
244-
groups[ i ] = "#" + nid + " " + toSelector( groups[ i ] );
254+
groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " +
255+
toSelector( groups[ i ] );
245256
}
246257
newSelector = groups.join( "," );
247-
248-
// Expand context for sibling selectors
249-
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
250-
context;
251258
}
252259

253260
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 = true;
13+
} catch ( e ) {}
14+
15+
return support;
16+
17+
} );

test/unit/selector.js

+35
Original file line numberDiff line numberDiff line change
@@ -1631,6 +1631,41 @@ QUnit.test( "context", function( assert ) {
16311631
}
16321632
} );
16331633

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

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: true
69+
},
70+
safari: {
71+
scope: true
72+
},
73+
firefox: {
74+
scope: true
75+
},
76+
ios: {
77+
scope: true
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)