]> source.dussan.org Git - jquery.git/commitdiff
Selector: Leverage the :scope pseudo-class where possible
authorMichał Gołębiowski-Owczarek <m.goleb@gmail.com>
Mon, 19 Aug 2019 16:41:03 +0000 (18:41 +0200)
committerGitHub <noreply@github.com>
Mon, 19 Aug 2019 16:41:03 +0000 (18:41 +0200)
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

src/selector.js
src/selector/support.js [new file with mode: 0644]
test/unit/selector.js
test/unit/support.js

index 913f7486f71fdae4fda2f45fd57136f63b3e3bde..3e187a159f6e80a76dc89a4ba0560cc1f2487c29 100644 (file)
@@ -4,12 +4,13 @@ define( [
        "./var/indexOf",
        "./var/pop",
        "./var/push",
+       "./selector/support",
 
        // The following utils are attached directly to the jQuery object.
        "./selector/contains",
        "./selector/escapeSelector",
        "./selector/uniqueSort"
-], function( jQuery, document, indexOf, pop, push ) {
+], function( jQuery, document, indexOf, pop, push, support ) {
 
 "use strict";
 
@@ -230,24 +231,30 @@ function find( 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 = jQuery.escapeSelector( nid );
-                                       } 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 = jQuery.escapeSelector( nid );
+                                               } 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 {
diff --git a/src/selector/support.js b/src/selector/support.js
new file mode 100644 (file)
index 0000000..86cd2d9
--- /dev/null
@@ -0,0 +1,17 @@
+define( [
+       "../var/document",
+       "../var/support"
+], function( document, support ) {
+
+"use strict";
+
+// Support: IE 9 - 11+, Edge 12 - 18+
+// IE/Edge don't support the :scope pseudo-class.
+try {
+       document.querySelectorAll( ":scope" );
+       support.scope = true;
+} catch ( e ) {}
+
+return support;
+
+} );
index d6bd62c73e6f01bf04cf87e3205643566ee63cac..2c17b868207dea1b71341b7e1f27f93cf34a76b0 100644 (file)
@@ -1631,6 +1631,41 @@ QUnit.test( "context", function( assert ) {
        }
 } );
 
+// Support: IE 11+, Edge 12 - 18+
+// IE/Edge don't support the :scope pseudo-class so they will trigger MutationObservers.
+// The test is skipped there.
+QUnit[
+       ( QUnit.isIE || /edge\//i.test( navigator.userAgent ) ) ?
+               "skip" :
+               "test"
+       ]( "selectors maintaining context don't trigger mutation observers", function( assert ) {
+       assert.expect( 1 );
+
+       var timeout,
+               done = assert.async(),
+               container = jQuery( "<div/>" ),
+               child = jQuery( "<div/>" );
+
+       child.appendTo( container );
+       container.appendTo( "#qunit-fixture" );
+
+       var observer = new MutationObserver(  function() {
+               clearTimeout( timeout );
+               observer.disconnect();
+               assert.ok( false, "Mutation observer fired during selection" );
+               done();
+       } );
+       observer.observe( container[ 0 ], { attributes: true } );
+
+       container.find( "div div" );
+
+       timeout = setTimeout( function() {
+               observer.disconnect();
+               assert.ok( true, "Mutation observer didn't fire during selection" );
+               done();
+       } );
+} );
+
 QUnit.test( "caching does not introduce bugs", function( assert ) {
        assert.expect( 3 );
 
index 266b02dd80023825e3851d0d5cb00d2d3cd60f76..8be82bbe9d92c4c5b38760dea9f03eb9528dc809 100644 (file)
@@ -58,12 +58,24 @@ testIframe(
        var expected,
                userAgent = window.navigator.userAgent,
                expectedMap = {
-                       edge: {},
-                       ie_11: {},
-                       chrome: {},
-                       safari: {},
-                       firefox: {},
-                       ios: {}
+                       edge: {
+                               scope: undefined
+                       },
+                       ie_11: {
+                               scope: undefined
+                       },
+                       chrome: {
+                               scope: true
+                       },
+                       safari: {
+                               scope: true
+                       },
+                       firefox: {
+                               scope: true
+                       },
+                       ios: {
+                               scope: true
+                       }
                };
 
        if ( /edge\//i.test( userAgent ) ) {
@@ -95,6 +107,15 @@ testIframe(
                        j++;
                }
 
+               // Add an assertion per undefined support prop as it may
+               // not even exist on computedSupport but we still want to run
+               // the check.
+               for ( prop in expected ) {
+                       if ( expected[ prop ] === undefined ) {
+                               j++;
+                       }
+               }
+
                assert.expect( j );
 
                for ( i in expected ) {
@@ -116,6 +137,15 @@ testIframe(
                        i++;
                }
 
+               // Add an assertion per undefined support prop as it may
+               // not even exist on computedSupport but we still want to run
+               // the check.
+               for ( prop in expected ) {
+                       if ( expected[ prop ] === undefined ) {
+                               i++;
+                       }
+               }
+
                assert.expect( i );
 
                // Record all support props and the failing ones and ensure every test
@@ -123,7 +153,7 @@ testIframe(
                for ( browserKey in expectedMap ) {
                        for ( supportTestName in expectedMap[ browserKey ] ) {
                                supportProps[ supportTestName ] = true;
-                               if ( expectedMap[ browserKey ][ supportTestName ] !== true ) {
+                               if ( !expectedMap[ browserKey ][ supportTestName ] ) {
                                        failingSupportProps[ supportTestName ] = true;
                                }
                        }