]> source.dussan.org Git - jquery-ui.git/commitdiff
Widget: Optimize attachment of the _untrackClassesElement listener
authorMichał Gołębiowski-Owczarek <m.goleb@gmail.com>
Sat, 15 Jan 2022 00:26:23 +0000 (01:26 +0100)
committerGitHub <noreply@github.com>
Sat, 15 Jan 2022 00:26:23 +0000 (01:26 +0100)
jQuery UI 1.13.0 changed the logic attaching the `_untrackClassesElement`
listener in the `_classes` widget method; one of the side effects was calling
`this._on` for each node that needed the listener. That caused a severe
performance degradation for large comboboxes as each `_on` jQuery UI call
causes a jQuery `add` call that calls Sizzle's `uniqueSort` underneath.

Instead, collect the nodes that need the listener and then, outside of the loop,
create a jQuery object out of them and attach the listener once. That's still
slower than the jQuery 1.12 version but only slightly: 936 ms to 1.03s on a very
large list on a recent MacBook Pro, compared to ~30 seconds before this patch.

Fixes gh-2014
Closes gh-2037

tests/unit/autocomplete/core.js
tests/unit/selectmenu/core.js
tests/unit/tabs/core.js
ui/widget.js

index a2a36cb4c1cd837bcf15f5aa0ce1fdb15025b52c..2083e16b1aac9887bd4ffa3827cb11a4bc3ff786 100644 (file)
@@ -443,4 +443,27 @@ QUnit.test( "Close on click outside when focus remains", function( assert ) {
        } );
 } );
 
+QUnit.test( "extra listeners created during typing (trac-15082, trac-15095)", function( assert ) {
+       assert.expect( 2 );
+
+       var origRemoveListenersCount;
+       var element = $( "#autocomplete" ).autocomplete( {
+               source: [ "java", "javascript" ],
+               delay: 0
+       } );
+
+       element.val( "j" ).autocomplete( "search", "j" );
+       origRemoveListenersCount = jQuery._data( element[ 0 ], "events" ).remove.length;
+
+       element.val( "ja" ).autocomplete( "search", "ja" );
+       assert.equal( jQuery._data( element[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after typing multiple letters" );
+
+       element.val( "jav" ).autocomplete( "search", "jav" );
+       assert.equal( jQuery._data( element[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after typing multiple letters" );
+} );
+
 } );
index 05a0e63011388f2f3409179e4c8e29e200513fbb..f32cec9b99afb3e0fa500d4f22ddb4a090c14004 100644 (file)
@@ -404,4 +404,34 @@ QUnit.test( "Options with hidden attribute should not be rendered", function( as
        } );
 } );
 
+QUnit.test( "extra listeners created after selection (trac-15078, trac-15152)", function( assert ) {
+       assert.expect( 3 );
+
+       var origRemoveListenersCount;
+       var element = $( "#speed" ).selectmenu();
+       var menu = element.selectmenu( "widget" );
+
+       element.val( "Slow" );
+       element.selectmenu( "refresh" );
+       origRemoveListenersCount = jQuery._data( menu[ 0 ], "events" ).remove.length;
+
+       element.val( "Fast" );
+       element.selectmenu( "refresh" );
+       assert.equal( jQuery._data( menu[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after typing multiple letters" );
+
+       element.val( "Faster" );
+       element.selectmenu( "refresh" );
+       assert.equal( jQuery._data( menu[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after typing multiple letters" );
+
+       element.val( "Slow" );
+       element.selectmenu( "refresh" );
+       assert.equal( jQuery._data( menu[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after typing multiple letters" );
+} );
+
 } );
index 8d4322463edd548f91ea01d1d46f6f47c2be7d61..37dd3d4e3019c691fabd94fd0d1216fc4e68ca89 100644 (file)
@@ -675,4 +675,56 @@ QUnit.test( "#4033 - IE expands hash to full url and misinterprets tab as ajax",
        state( assert, element, 1 );
 } );
 
+
+QUnit.test( "extra listeners created when tabs are added/removed (trac-15136)", function( assert ) {
+       assert.expect( 3 );
+
+       var origRemoveListenersCount;
+       var element = $( "#tabs1" ).tabs();
+       var tabCounter = 10;
+
+       function addTab() {
+               var label = "Tab " + tabCounter;
+               var id = "tabs-" + tabCounter;
+               var li = $(
+                       "<li>" +
+                       "       <a href='#" + id + "'>" + label + "</a> " +
+                       "       <span class='ui-icon ui-icon-close' role='presentation'>Remove Tab</span>" +
+                       "</li>"
+               );
+               var tabContentHtml = "Tab " + tabCounter + " content.";
+
+               element.find( ".ui-tabs-nav" ).append( li );
+               element.append( "<div id='" + id + "'><p>" + tabContentHtml + "</p></div>" );
+               element.tabs( "refresh" );
+               tabCounter++;
+       }
+
+       function removeLastTab() {
+               element.find( ".ui-icon-close" ).last().trigger( "click" );
+       }
+
+       origRemoveListenersCount = jQuery._data( element[ 0 ], "events" ).remove.length;
+
+       addTab();
+       assert.equal( jQuery._data( element[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after adding a new tab" );
+
+       addTab();
+       addTab();
+       addTab();
+       assert.equal( jQuery._data( element[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after adding multiple tabs" );
+
+       removeLastTab();
+       removeLastTab();
+       removeLastTab();
+       removeLastTab();
+       assert.equal( jQuery._data( element[ 0 ], "events" ).remove.length,
+               origRemoveListenersCount,
+               "No extra listeners after removing all the extra tabs" );
+} );
+
 } );
index 04daaa88309506aad5029fc3feb099d9c0efdc5f..69240f92ceb86c8930b8d4144cbac55e0e8b948f 100644 (file)
@@ -499,6 +499,8 @@ $.Widget.prototype = {
                }, options );
 
                function bindRemoveEvent() {
+                       var nodesToBind = [];
+
                        options.element.each( function( _, element ) {
                                var isTracked = $.map( that.classesElementLookup, function( elements ) {
                                        return elements;
@@ -508,11 +510,13 @@ $.Widget.prototype = {
                                        } );
 
                                if ( !isTracked ) {
-                                       that._on( $( element ), {
-                                               remove: "_untrackClassesElement"
-                                       } );
+                                       nodesToBind.push( element );
                                }
                        } );
+
+                       that._on( $( nodesToBind ), {
+                               remove: "_untrackClassesElement"
+                       } );
                }
 
                function processClassString( classes, checkOption ) {