Skip to content

Commit 7677a6a

Browse files
committed
fix(perf): remove need for node sorting from select completely
1 parent 189c165 commit 7677a6a

File tree

5 files changed

+55
-38
lines changed

5 files changed

+55
-38
lines changed

lib/core/base/context.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ function validateContext(context) {
200200
* @param {Object} spec Configuration or "specification" object
201201
*/
202202
function Context(spec) {
203+
//jshint maxstatements:18
203204
'use strict';
204205
var self = this;
205206

@@ -229,4 +230,8 @@ function Context(spec) {
229230
if (err instanceof Error) {
230231
throw err;
231232
}
233+
if (!Array.isArray(this.include)) {
234+
this.include = Array.from(this.include);
235+
}
236+
this.include.sort(axe.utils.nodeSorter); // ensure that the order of the include nodes is document order
232237
}

lib/core/utils/select.js

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,17 @@ function pushNode(result, nodes) {
6868
}
6969

7070
/**
71-
* returns true if any of the nodes in the list is a parent of another node in the list
71+
* reduces the includes list to only the outermost includes
7272
* @param {Array} the array of include nodes
73-
* @return {Boolean}
73+
* @return {Array} the modified array of nodes
7474
*/
75-
function hasOverlappingIncludes(includes) {
76-
let list = includes.slice();
77-
while (list.length > 1) {
78-
let last = list.pop();
79-
if (list[list.length - 1].actualNode.contains(last.actualNode)) {
80-
return true;
75+
function reduceIncludes(includes) {
76+
return includes.reduce((res, el) => {
77+
if (!res.length || !res[res.length - 1].actualNode.contains(el.actualNode)) {
78+
res.push(el);
8179
}
82-
}
83-
return false;
80+
return res;
81+
}, []);
8482
}
8583

8684
/**
@@ -94,10 +92,6 @@ axe.utils.select = function select(selector, context) {
9492
'use strict';
9593

9694
var result = [], candidate;
97-
if (!Array.isArray(context.include)) {
98-
context.include = Array.from(context.include);
99-
}
100-
context.include.sort(axe.utils.nodeSorter); // ensure that the order of the include nodes is document order
10195
if (axe._selectCache) { // if used outside of run, it will still work
10296
for (var j = 0, l = axe._selectCache.length; j < l; j++) {
10397
// First see whether the item exists in the cache
@@ -112,18 +106,16 @@ axe.utils.select = function select(selector, context) {
112106
return isNodeInContext(node, context);
113107
};
114108
})(context);
115-
for (var i = 0; i < context.include.length; i++) {
116-
candidate = context.include[i];
109+
var reducedIncludes = reduceIncludes(context.include);
110+
for (var i = 0; i < reducedIncludes.length; i++) {
111+
candidate = reducedIncludes[i];
117112
if (candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE &&
118113
axe.utils.matchesSelector(candidate.actualNode, selector) &&
119114
curried(candidate)) {
120115
result = pushNode(result, [candidate]);
121116
}
122117
result = pushNode(result, axe.utils.querySelectorAllFilter(candidate, selector, curried));
123118
}
124-
if (context.include.length > 1 && hasOverlappingIncludes(context.include)) {
125-
result.sort(axe.utils.nodeSorter);
126-
}
127119
if (axe._selectCache) {
128120
axe._selectCache.push({
129121
selector: selector,

test/core/base/context.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ describe('Context', function() {
9898
[$id('foo'), $id('bar')]);
9999
});
100100

101+
it('should sort the include nodes in document order', function() {
102+
fixture.innerHTML = '<div id="foo"><div id="bar"></div></div><div id="baz"></div>';
103+
104+
var result = new Context([
105+
['#foo'],
106+
['#baz'],
107+
['#bar']
108+
]);
109+
110+
assert.deepEqual(result.include.map(function (n) { return n.actualNode; }),
111+
[$id('foo'), $id('bar'), $id('baz')]);
112+
});
113+
101114
it('should remove any null reference', function() {
102115
fixture.innerHTML = '<div id="foo"><div id="bar"></div></div>';
103116

test/core/base/rule.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,9 +521,18 @@ describe('Rule', function() {
521521
evaluate: function() {},
522522
id: 'cats'
523523
}]
524+
}, {
525+
checks: {
526+
cats: {
527+
run: function (node, options, resolve) {
528+
success = true;
529+
resolve(true);
530+
}
531+
}
532+
}
524533
});
525534
rule.run({
526-
include: axe.utils.getFlattenedTree(document)[0]
535+
include: [axe.utils.getFlattenedTree(document)[0]]
527536
}, {}, noop, isNotCalled);
528537
assert.isTrue(success);
529538

@@ -538,9 +547,18 @@ describe('Rule', function() {
538547
evaluate: function() {},
539548
id: 'cats'
540549
}]
550+
}, {
551+
checks: {
552+
cats: {
553+
run: function (node, options, resolve) {
554+
success = true;
555+
resolve(true);
556+
}
557+
}
558+
}
541559
});
542560
rule.run({
543-
include: axe.utils.getFlattenedTree(document)[0]
561+
include: [axe.utils.getFlattenedTree(document)[0]]
544562
}, {}, function() {
545563
success = true;
546564
}, isNotCalled);

test/core/utils/select.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,30 +125,19 @@ describe('axe.utils.select', function () {
125125

126126
});
127127

128-
it('should sort by DOM order', function () {
129-
fixture.innerHTML = '<div id="one"><div id="target1" class="bananas"></div></div>' +
130-
'<div id="two"><div id="target2" class="bananas"></div></div>';
131-
132-
var result = axe.utils.select('.bananas', { include: [axe.utils.getFlattenedTree($id('two'))[0],
133-
axe.utils.getFlattenedTree($id('one'))[0]] });
134-
135-
assert.deepEqual(result.map(function (n) { return n.actualNode; }),
136-
[$id('target1'), $id('target2')]);
137-
138-
});
139-
140-
it('should sort by DOM order on overlapping elements', function () {
128+
it('should not return duplicates on overlapping includes', function () {
141129
fixture.innerHTML = '<div id="zero"><div id="one"><div id="target1" class="bananas"></div></div>' +
142130
'<div id="two"><div id="target2" class="bananas"></div></div></div>';
143131

144-
var result = axe.utils.select('.bananas', { include: [axe.utils.getFlattenedTree($id('one'))[0],
145-
axe.utils.getFlattenedTree($id('zero'))[0]] });
132+
var result = axe.utils.select('.bananas', { include: [axe.utils.getFlattenedTree($id('zero'))[0],
133+
axe.utils.getFlattenedTree($id('one'))[0]] });
146134

147135
assert.deepEqual(result.map(function (n) { return n.actualNode; }),
148-
[$id('target1'), $id('target1'), $id('target2')]);
149-
assert.equal(result.length, 3);
136+
[$id('target1'), $id('target2')]);
137+
assert.equal(result.length, 2);
150138

151139
});
140+
152141
it ('should return the cached result if one exists', function () {
153142
fixture.innerHTML = '<div id="zero"><div id="one"><div id="target1" class="bananas"></div></div>' +
154143
'<div id="two"><div id="target2" class="bananas"></div></div></div>';

0 commit comments

Comments
 (0)