Skip to content

Commit c2e2519

Browse files
committed
Widget: Increase performance for large sortable/draggable collections
Fixes jquerygh-2062
1 parent f76bbcd commit c2e2519

File tree

3 files changed

+46
-29
lines changed

3 files changed

+46
-29
lines changed

tests/unit/widget/classes.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,27 @@ QUnit.test( "Classes elements are untracked as they are removed from the DOM", f
155155
var widget = $( "#widget" ).classesWidget();
156156
var instance = widget.classesWidget( "instance" );
157157

158-
assert.equal( instance.classesElementLookup[ "ui-classes-span" ].length, 3,
158+
assert.equal( instance.classesElementLookup[ "ui-classes-span" ].size, 3,
159159
"Widget is tracking 3 ui-classes-span elements" );
160-
assert.equal( instance.classesElementLookup[ "ui-core-span-null" ].length, 3,
160+
assert.equal( instance.classesElementLookup[ "ui-core-span-null" ].size, 3,
161161
"Widget is tracking 3 ui-core-span-null elements" );
162-
assert.equal( instance.classesElementLookup[ "ui-core-span" ].length, 3,
162+
assert.equal( instance.classesElementLookup[ "ui-core-span" ].size, 3,
163163
"Widget is tracking 3 ui-core-span elements" );
164164

165165
widget.find( "span" ).eq( 0 ).remove();
166-
assert.equal( instance.classesElementLookup[ "ui-classes-span" ].length, 2,
166+
assert.equal( instance.classesElementLookup[ "ui-classes-span" ].size, 2,
167167
"After removing 1 span from dom 2 ui-classes-span elements are tracked" );
168-
assert.equal( instance.classesElementLookup[ "ui-core-span-null" ].length, 2,
168+
assert.equal( instance.classesElementLookup[ "ui-core-span-null" ].size, 2,
169169
"After removing 1 span from dom 2 ui-core-span-null elements are tracked" );
170-
assert.equal( instance.classesElementLookup[ "ui-core-span" ].length, 2,
170+
assert.equal( instance.classesElementLookup[ "ui-core-span" ].size, 2,
171171
"After removing 1 span from dom 2 ui-core-span elements are tracked" );
172172

173173
widget.find( "span" ).remove();
174-
assert.equal( instance.classesElementLookup[ "ui-classes-span" ].length, 0,
174+
assert.equal( instance.classesElementLookup[ "ui-classes-span" ].size, 0,
175175
"No ui-classes-span elements are tracked after removing all spans" );
176-
assert.equal( instance.classesElementLookup[ "ui-core-span-null" ].length, 0,
176+
assert.equal( instance.classesElementLookup[ "ui-core-span-null" ].size, 0,
177177
"No ui-core-span-null elements are tracked after removing all spans" );
178-
assert.equal( instance.classesElementLookup[ "ui-core-span" ].length, 0,
178+
assert.equal( instance.classesElementLookup[ "ui-core-span" ].size, 0,
179179
"No ui-core-span elements are tracked after removing all spans" );
180180
} );
181181

ui/.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99

1010
"env": {
11+
"es6": true,
1112
"browser": true,
1213
"jquery": true,
1314
"node": false

ui/widget.js

+36-20
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,10 @@ $.Widget.prototype = {
301301
this.uuid = widgetUuid++;
302302
this.eventNamespace = "." + this.widgetName + this.uuid;
303303

304-
this.bindings = $();
304+
this.bindings = new Set();
305305
this.hoverable = $();
306306
this.focusable = $();
307-
this.classesElementLookup = {};
307+
this.classesElementLookup = Object.create( null );
308308

309309
if ( element !== this ) {
310310
$.data( element, this.widgetFullName, this );
@@ -355,7 +355,7 @@ $.Widget.prototype = {
355355

356356
this._destroy();
357357
$.each( this.classesElementLookup, function( key, value ) {
358-
that._removeClass( value, key );
358+
that._removeClass( $( Array.from( value ) ), key );
359359
} );
360360

361361
// We can probably remove the unbind calls in 2.0
@@ -368,7 +368,7 @@ $.Widget.prototype = {
368368
.removeAttr( "aria-disabled" );
369369

370370
// Clean up events and states
371-
this.bindings.off( this.eventNamespace );
371+
$( Array.from( this.bindings ) ).off( this.eventNamespace );
372372
},
373373

374374
_destroy: $.noop,
@@ -450,16 +450,12 @@ $.Widget.prototype = {
450450
currentElements = this.classesElementLookup[ classKey ];
451451
if ( value[ classKey ] === this.options.classes[ classKey ] ||
452452
!currentElements ||
453-
!currentElements.length ) {
453+
!currentElements.size ) {
454454
continue;
455455
}
456456

457-
// We are doing this to create a new jQuery object because the _removeClass() call
458-
// on the next line is going to destroy the reference to the current elements being
459-
// tracked. We need to save a copy of this collection so that we can add the new classes
460-
// below.
461-
elements = $( currentElements.get() );
462-
this._removeClass( currentElements, classKey );
457+
elements = $( Array.from( currentElements ) );
458+
this._removeClass( elements, classKey );
463459

464460
// We don't use _addClass() here, because that uses this.options.classes
465461
// for generating the string of classes. We want to use the value passed in from
@@ -509,7 +505,7 @@ $.Widget.prototype = {
509505
return elements;
510506
} )
511507
.some( function( elements ) {
512-
return elements.is( element );
508+
return elements.has( element );
513509
} );
514510

515511
if ( !isTracked ) {
@@ -525,12 +521,24 @@ $.Widget.prototype = {
525521
function processClassString( classes, checkOption ) {
526522
var current, i;
527523
for ( i = 0; i < classes.length; i++ ) {
528-
current = that.classesElementLookup[ classes[ i ] ] || $();
524+
current = that.classesElementLookup[ classes[ i ] ] || new Set();
529525
if ( options.add ) {
530526
bindRemoveEvent();
531-
current = $( $.uniqueSort( current.get().concat( options.element.get() ) ) );
527+
528+
// This function is invoked synchronously, so the reference
529+
// to `current` is not an issue.
530+
// eslint-disable-next-line no-loop-func
531+
$.each( options.element, function( _i, node ) {
532+
current.add( node );
533+
} );
532534
} else {
533-
current = $( current.not( options.element ).get() );
535+
536+
// This function is invoked synchronously, so the reference
537+
// to `current` is not an issue.
538+
// eslint-disable-next-line no-loop-func
539+
$.each( options.element, function( _i, node ) {
540+
current.delete( node );
541+
} );
534542
}
535543
that.classesElementLookup[ classes[ i ] ] = current;
536544
full.push( classes[ i ] );
@@ -553,9 +561,7 @@ $.Widget.prototype = {
553561
_untrackClassesElement: function( event ) {
554562
var that = this;
555563
$.each( that.classesElementLookup, function( key, value ) {
556-
if ( $.inArray( event.target, value ) !== -1 ) {
557-
that.classesElementLookup[ key ] = $( value.not( event.target ).get() );
558-
}
564+
value.delete( event.target );
559565
} );
560566

561567
this._off( $( event.target ) );
@@ -583,6 +589,7 @@ $.Widget.prototype = {
583589
},
584590

585591
_on: function( suppressDisabledCheck, element, handlers ) {
592+
var i;
586593
var delegateElement;
587594
var instance = this;
588595

@@ -600,7 +607,11 @@ $.Widget.prototype = {
600607
delegateElement = this.widget();
601608
} else {
602609
element = delegateElement = $( element );
603-
this.bindings = this.bindings.add( element );
610+
for ( i = 0; i < element.length; i++ ) {
611+
$.each( element, function( _i, node ) {
612+
instance.bindings.add( node );
613+
} );
614+
}
604615
}
605616

606617
$.each( handlers, function( event, handler ) {
@@ -637,12 +648,17 @@ $.Widget.prototype = {
637648
},
638649

639650
_off: function( element, eventName ) {
651+
var that = this;
652+
640653
eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) +
641654
this.eventNamespace;
642655
element.off( eventName );
643656

657+
$.each( element, function( _i, node ) {
658+
that.bindings.delete( node );
659+
} );
660+
644661
// Clear the stack to avoid memory leaks (#10056)
645-
this.bindings = $( this.bindings.not( element ).get() );
646662
this.focusable = $( this.focusable.not( element ).get() );
647663
this.hoverable = $( this.hoverable.not( element ).get() );
648664
},

0 commit comments

Comments
 (0)