Related
This was given to me as an interview question -- didn't get the job, but I still want to figure it out.
The objective is to write two querySelectorAll functions: one called qsa1 which works for selectors consisting of a single tag name (e.g. div or span) and another called qsa2 which accepts arbitrarily nested tag selectors (such as p span or ol li code).
I got the first one easily enough, but the second one is a bit trickier.
I suspect that, in order to handle a variable number of selectors, the proper solution might be recursive, but I figured I'd try to get something working that is iterative first. Here's what I've got so far:
qsa2 = function(node, selector) {
var selectors = selector.split(" ");
var matches;
var children;
var child;
var parents = node.getElementsByTagName(selectors[0]);
if (parents.length > 0) {
for (var i = 0; i < parents.length; i++) {
children = parents[i].getElementsByTagName(selectors[1]);
if (children.length > 0) {
for (var i = 0; i < parents.length; i++) {
child = children[i];
matches.push(child); // somehow store our result here
}
}
}
}
return matches;
}
The first problem with my code, aside from the fact that it doesn't work, is that it only handles two selectors (but it should be able to clear the first, second, and fourth cases).
The second problem is that I'm having trouble returning the correct result. I know that, just as in qsa1, I should be returning the same result as I'd get by calling the getElementsByTagName() function which "returns a live NodeList of elements with the given tag name". Creating an array and pushing or appending the Nodes to it isn't cutting it.
How do I compose the proper return result?
(For context, the full body of code can be found here)
Here's how I'd do it
function qsa2(selector) {
var next = document;
selector.split(/\s+/g).forEach(function(sel) {
var arr = [];
(Array.isArray(next) ? next : [next]).forEach(function(el) {
arr = arr.concat( [].slice.call(el.getElementsByTagName(sel) ));
});
next = arr;
});
return next;
}
Assume we always start with the document as context, then split the selector on spaces, like you're already doing, and iterate over the tagnames.
On each iteration, just overwrite the outer next variable, and run the loop again.
I've used an array and concat to store the results in the loop.
This is somewhat similar to the code in the question, but it should be noted that you never create an array, in fact the matches variable is undefined, and can't be pushed to.
You have syntax errors here:
if (parents.length > 0) {
for (var i = 0; i < parents.length; i++) {
children = parents[i].getElementsByTagName(selectors[1]);
if (children.length > 0) {
for (var i = 0; i < parents.length; i++) { // <-----------------------
Instead of going over the length of the children, you go over the length of the parent.
As well as the fact that you are reusing iteration variable names! This means the i that's mapped to the length of the parent is overwritten in the child loop!
On a side note, a for loop won't iterate over the elements if it's empty anyway, so your checks are redundant.
It should be the following:
for (var i = 0; i < parents.length; i++) {
children = parents[i].getElementsByTagName(selectors[1]);
for (var k = 0; k < children.length; i++) {
Instead of using an iterative solution, I would suggest using a recursive solution like the following:
var matches = [];
function recursivelySelectChildren(selectors, nodes){
if (selectors.length != 0){
for (var i = 0; i < nodes.length; i++){
recursivelySelectChildren(nodes[i].getElementsByTagName(selectors[0]), selectors.slice(1))
}
} else {
matches.push(nodes);
}
}
function qsa(selector, node){
node = node || document;
recursivelySelectChildren(selector.split(" "), [node]);
return matches;
}
Say I have the following HTML (condensed):
<div><div><div><ul><li>Text</li></ul></div></div></div>
<div><div><div><ul><li>Text 2</li></ul></div></div></div>
<div><div><div><ul><li>Text 3</li></ul></div></div></div>
I want to remove the lowest child elements first, until ultimately removing the parent, then move on to the next parent element and its children. This can be easily accomplished by a simple loop that goes through each child element, removes it, then removes the next child element (i.e. parent of the previous child):
var children = $("body").find("*");
var i = children.length;
function loop() {
$(children[i]).remove();
i--;
if (i > -1) {
setTimeout(loop, 20);
}
}
loop();
The problem with this, however, is that it removes the child elements from the lowest parent element first. If you were to run this code with my test markup, you could see what I mean.
I want to remove the child elements from the top most parent, then work my way down, therefore reversing the order of the above code. I was able to somewhat accomplish this with the following code:
var parents = $("body").children(":not(:empty)");
var i = 0;
var speed = 1000;
function loop() {
var children = $(parents[i]).find("*");
var x = children.length;
function inside() {
$(children[x]).remove();
x--;
if (x > -1) {
setTimeout(inside, speed);
} else if (i < parents.length) {
$(parents[i - 1]).remove();
loop();
} else if (i === parents.length) {
$(parents[i - 1]).remove();
}
}
inside();
i++;
}
loop();
The problem with this code, however, is that it only reverses the order of deleting with respect to the parent element. If there are multiple child elements within a parent, it will still delete them in the default ascending order (bottom to top).
My question, therefore, is how can I delete all the elements in descending order, regardless of how many child elements there are, in a much cleaner fashion? There has to be a much better approach than what I attempted. jQuery isn't a requirement either. The reason for the setTimeouts is because I need a delay between removing the elements. As usual, I probably overlooked something relatively simple, so bear with me.
To reiterate, if the HTML looks like this:
<div>
<div>Child 1</div>
<div>Child 2</div>
<div>
<div>Child 3</div>
<div>Child 4</div>
</div>
</div>
I would want it to be deleted in the following order:
Child 1
Child 2
Child 3
Child 4
First build a post-order (aka child first) version of the DOM tree using the following recursive function:
var nodes = [];
function generate()
{
$(this).children().each(generate);
nodes.push(this);
}
generate.call($('body'));
Then, iterate as per normal:
var i = 0;
function loop()
{
$(nodes[i]).remove();
if (++i < nodes.length) {
setTimeout(loop, 1000);
}
}
loop();
Demo
Algorithm idea in pseudocode:
RemoveNode( node) {
for(i=node.children.length-1;i>=0;i--){
RemoveNode(node.children[i]);
}
remove(self);
return;
}
Added actual code according to pseudocode:
function RemoveNode(node){
for(var i = node.children.length - 1; i >= 0; i--){
RemoveNode(node.children[i]);
}
$(node).remove();
}
Pseudocode with breaks to see how algorithm works. No idea why, but I can't make it work with delay.
function RemoveNode(node){
for(var i = node.children.length - 1; i >= 0; i--){
RemoveNode(node.children[i]);
alert("hi");
}
$(node).remove();
}
RemoveNode($(".parent")[0]);
I think this does what you want:
removeLast();
function removeLast(){
var o = document.getElementById("root"), p = o;
while (p.lastChild) p = p.lastChild;
p.parentNode.removeChild(p);
if(o != p) setTimeout(removeLast,20);
}
Fiddle
I'm not sure I understand the question fully, but maybe this is what you're looking for?
Demo: http://jsfiddle.net/xT3Au/1/
var $parents = $('.parent');
$parents.each(function(i) {
var $tree = $(this).find('*').addBack();
$tree.sort(function(a, b) {
return $(a).find('*').length - $(b).find('*').length;
}).each(function(j, el) {
var speed = 1000 * (j + 1 + i * $tree.length);
setTimeout(function(){ $(el).remove() }, speed);
});
});
I think using classes to know which parents should follow which order would be easier. Otherwise it would be quite difficult to figure out the order. Try playing with the sorting function in any case if I got it wrong.
Let's suppose I have a <select> element:
<select id="foobar" name="foobar" multiple="multiple">
<option value="1">Foobar 1</option>
<option value="2">Foobar 2</option>
<option value="3">Foobar 3</option>
</select>
And let's suppose I have an array of values, something like:
var optionValues = [2, 3];
How can I select the <option>s with values 2 and 3 most efficiently?
I'm working with a <select> that has thousands of <option>s, so doing it manually like this won't work:
var optionElements = [];
$("#foobar").children().each(function() {
if($.inArray($(this).val(), optionValues)) {
optionElements.push($(this));
}
}
It's just too slow. Is there a way to hand jQuery a list of values for the elements I need to select? Any ideas?
P.S. In case you're wondering, I am in the middle of optimizing my jQuery PickList widget which currently sucks at handling large lists.
Have you considered creating a big hashtable at plugin bootstrap? Granted values are unique:
var options = {};
$('#foobar').children().each(function(){
options[this.value] = this;
});
This way looking up is straightforward - options[valueNeeded].
EDIT - searching for optionValues:
var optionValues = [2, 3];
var results = [];
for(i=0; i<optionValues.length;i++){
results.push[ options[ optionValues[i] ] ];
}
This hasn't been profiled so take it with a grain shaker of salt:
var options = $("some-select").children(),
toFind = [2, 3],
values = {},
selectedValues = [],
unSelectedValues = [];
// First, make a lookup table of selectable values
// O(1) beats O(n) any day
for (i=0, l=toFind.length; i++; i<l) {
values[toFind[i]] = true;
}
// Avoid using more complicated constructs like `forEach` where speed is critical
for (i=0, l=options.length; i++; i<l) {
// Avoid nasty edge cases since we need to support *all* possible values
// See: http://www.devthought.com/2012/01/18/an-object-is-not-a-hash/
if (values[options[i]] === true) {
selectedValues.push(options[i]);
}
else {
unSelectedValues.push(options[i]);
}
}
There is obviously more we can do (like caching the selected and unselected values so we can avoid rebuilding them every time the user moves a value between them) and if we assume that the data is all unique we could even turn the whole thing into three "hashes" - but whatever we do we should profile it and ensure that it really is as fast as we think it is.
Assuming the values are unique, you can take some shortcuts. For instance, once you have found a value you can stop searching for it by splice()ing it off the search array.
This would be the ultimate optimisation, though, taking you from O(n^2) all the way down to O(n log n): Sorting.
First, loop through the options and build an array. Basically you just want to convert the NodeList to an Array. Then, sort the array with a callback to fetch the option's value. Sort the search array. Now you can loop through the "options" array and look for the current smallest search item.
var optsNodeList = document.getElementById('foobar').options,
optsArray = [], l = optsNodeList.length, i,
searchArray = [2,3], matches = [], misses = [];
for( i=0; i<l; i++) optsArray[i] = optsNodeList[i];
optsArray.sort(function(a,b) {return a.value < b.value ? -1 : 1;});
searchArray.sort();
while(searchArray[0] && (i = optsArray.shift())) {
while( i > searchArray[0]) {
misses.push(searchArray.shift());
}
if( i == searchArray[0]) {
matches.push(i);
searchArray.shift();
}
}
Try this:
var $found = [];
var notFound = [];
var $opt = $('#foobar option');
$.each(optionValues, function(i, v){
var $this = $opt.filter('[value='+v+']');
if ($this.length) {
$elems.push($this)
} else {
notFound.push(v);
}
})
First of all, I want to thank you all for the awesome responses! I'm considering each one, and I will probably do benchmarks before I make a decision.
In the interim, I actually found an "acceptable" solution based on this answer to another question.
Here's what I came up with (the last block, with the custom filter() implementation, is where the magic happens):
var items = self.sourceList.children(".ui-selected");
var itemIds = [];
items.each(function()
{
itemIds.push( this.value );
});
self.element.children().filter(function()
{
return $.inArray(this.value, itemIds) != -1;
}).attr("selected", "selected");
I doubt this is as efficient as any of the stuff you guys posted, but it has decreased the "Add" picklist operation time from about 10 seconds to 300ms on a 1500 item list.
I would give jQuery's filter() method a try, something like:
var matches = filter(function() {
// Determine if "this" is a match and return true/false appropriately
});
// Do something with the matches
matches.addClass('foobar');
It may not be the fastest solution here, but it is fairly optimized and very very simple without having to keep track of lists and all that jazz. It should be fast enough for your situation.
Try this.
var optionValues = [2, 3],
elements = [],
options = document.getElementById('foobar').options;
var i = 0;
do {
var option = options[i];
if(optionValues.indexOf(+option.value) != -1) {
elements.push(option);
}
} while(i++ < options.length - 1);
Let optionValues by an array of indexes to be selected.
for(var i = 0; i < optionValues.length; i++) {
document.forms[0].foobar.options[optionValues[i]].selected = true;
}
If you just want to select by value, the following should be suitable. It only loops over the options once and doesn't call any other functions, only one built–in method so it should be quick.
function selectMultiByValue(el, valuesArr) {
var opts = el.options;
var re = new RegExp('^(' + valuesArr.join('|') + ')$');
// Select options
for (var i=0, iLen=opts.length; i<iLen; i++) {
opts[i].selected = re.test(opts[i].value);
}
}
In some browsers, looping over a collection is slow so it may pay to convert the options collection to an array first. But test before doing that, it may not be worth it.
Note that if the select isn't a multiple select, only the option with the last listed value will be selected.
You may need to fiddle with the regular expression if you want to allow various other characters or cases.
As my last question was closed for being "too vague" - here it is again, with better wording.
I have a "grid" of li's that are loaded dynamically (through JavaScript/jQuery), the Array isn't huge but seems to take forever loading.
So, SO people - my question is:
Am I being stupid or is this code taking longer than it should to execute?
Live demo: http://jsfiddle.net/PrPvM/
(very slow, may appear to hang your browser)
Full code (download): http://www.mediafire.com/?xvd9tz07h2u644t
Snippet (from the actual array loop):
var gridContainer = $('#container');
var gridArray = [
2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
];
function loadMap() {
var i = 0;
while (i <= gridArray.length) {
var gridHTML = $(gridContainer).html();
$(gridContainer).html(gridHTML+'<li class="node"></li>');
i++;
}
$('li.node').each(function() {
$(gridArray).each(function (i, val) {
if (val == '0') { gridTile = 'grass.jpg' };
if (val == '1') { gridTile = 'mud.jpg' };
if (val == '2') { gridTile = 'sand.gif' };
$($('ul#container :nth-child('+i+')'))
.css({ 'background-image': 'url(img/tiles/'+gridTile });
});
});
}
The loop where you set the background images is the real problem. Look at it: you're looping through all the <li> elements that you just got finished building from the "grid". Then, inside that loop — that is, for each <li> element — you go through the entire "grid" array and reset the background. Each node will end up being set to the exact same thing: the background corresponding to the last thing in the array over and over again to the exact same background.
The way you build the HTML is also very inefficient. You should loop through the grid and build up a string array with an <li> element in each array slot. Actually, now that I think of it, you really should be doing the first and second loops at the same time.
function loadMap() {
var html = [], bg = ['grass', 'mud', 'sand'];
for (var i = 0, len = gridArray.length; i < len; ++i) {
html.push("<li class='node " + bg[gridArray[i]] + "'></li>");
}
$(gridContainer).html(html.join(''));
}
Now you'll also need some CSS rules:
li.grass { background-image: url(grass.jpg); }
li.mud { background-image: url(mud.jpg); }
li.sand { background-image: url(sand.gif); }
It'd probably be farm more efficient to build up the complete HTML for the array and then assign it to the .html property of the container, rather than assigning each individual li:
var gridHTML = $(gridContainer).html();
while (i <= gridArray.length) {
gridHTML = gridHTML+'<li class="node"></li>';
i++;
}
$(gridContainer).html();
Next, why are you looping over both of these? The outer loop is probably completely unnecessary, because your inner loop already uses nth-child to select the proper node.
$('li.node').each(function() {
$(gridArray).each(function (i, val) {
I have an array which is part of a small JS game I am working on I need to check (as often as reasonable) that each of the elements in the array haven't left the "stage" or "playground", so I can remove them and save the script load
I have coded the below and was wondering if anyone knew a faster/more efficient way to calculate this. This is run every 50ms (it deals with the movement).
Where bots[i][1] is movement in X and bots[i][2] is movement in Y (mutually exclusive).
for (var i in bots) {
var left = parseInt($("#" + i).css("left"));
var top = parseInt($("#" + i).css("top"));
var nextleft = left + bots[i][1];
var nexttop = top + bots[i][2];
if(bots[i][1]>0&&nextleft>=PLAYGROUND_WIDTH) { remove_bot(i); }
else if(bots[i][1]<0&&nextleft<=-GRID_SIZE) { remove_bot(i); }
else if(bots[i][2]>0&&nexttop>=PLAYGROUND_HEIGHT) { remove_bot(i); }
else if(bots[i][2]<0&&nexttop<=-GRID_SIZE) { remove_bot(i); }
else {
//alert(nextleft + ":" + nexttop);
$("#" + i).css("left", ""+(nextleft)+"px");
$("#" + i).css("top", ""+(nexttop)+"px");
}
}
On a similar note the remove_bot(i); function is as below, is this correct (I can't splice as it changes all the ID's of the elements in the array.
function remove_bot(i) {
$("#" + i).remove();
bots[i] = false;
}
Many thanks for any advice given!
Cache $("#" + i) in a variable; each time you do this, a new jQuery object is being created.
var self = $('#' + i);
var left = parseInt(self.css("left"));
var top = parseInt(self.css("top"));
Cache bots[i] in a variable:
var current = bots[i];
var nextleft = left + current[1];
var nexttop = top + current[2];
Store (cache) the jQuery object of the DOM element within the bot representation. At the moment it's been created every 50ms.
What I mean by this is that for every iteration of the loop, you're doing $('#' + i). Every time you call this, jQuery is building a jQuery object of the DOM element. This is far from trivial compared to other aspects of JS. DOM traversal/ manipulation is by far the slowest area of JavaScript.
As the result of $('#' + i) never changes for each bot, why not store the result within the bot? This way $('#' + i) gets executed once, instead of every 50ms.
In my example below, I've stored this reference in the element attribute of my Bot objects, but you can add it your bot (i.e in bots[i][3])
Store (cache) the position of the DOM element representing the bot within the bot representation, so the CSS position doesn't have to be calculated all the time.
On a side note, for (.. in ..) should be strictly used for iterating over objects, not arrays. Arrays should be iterated over using for (..;..;..)
Variables are extremely cheap in JavaScript; abuse them.
Here's an implementation I'd choose, which incorporates the suggestions I've made:
function Bot (x, y, movementX, movementY, playground) {
this.x = x;
this.y = y;
this.element = $('<div class="bot"/>').appendTo(playground);
this.movementX = movementX;
this.movementY = movementY;
};
Bot.prototype.update = function () {
this.x += this.movementX,
this.y += this.movementY;
if (this.movementX > 0 && this.x >= PLAYGROUP_WIDTH ||
this.movementX < 0 && this.x <= -GRID_SIZE ||
this.movementY > 0 && this.y >= PLAYGROUND_HEIGHT ||
this.movementY < 0 && this.y <= -GRIDSIZE) {
this.remove();
} else {
this.element.css({
left: this.x,
right: this.y
});
};
};
Bot.prototype.remove = function () {
this.element.remove();
// other stuff?
};
var playground = $('#playground');
var bots = [new Bot(0, 0, 1, 1, playground), new Bot(0, 0, 5, -5, playground), new Bot(10, 10, 10, -10, playground)];
setInterval(function () {
var i = bots.length;
while (i--) {
bots[i].update();
};
}, 50);
You're using parseInt. As far as I know, a bitwise OR 0 is faster than parseInt. So you could write
var left = $("#" + i).css("left") | 0;
instead.
Furthermore, I wouldn't make use of jQuery functions to obtain values like these every 50 ms, as there's always a bit more overhead when using those (the $ function has to parse its arguments, etc.). Just use native JavaScript functions to optimize these lines. Moreover, with your code, the element with id i has to be retrieved several times. Store those elements in a variable:
var item = document.getElementById(i);
var iStyle = item.style;
var left = iStyle.left;
…
(Please note that I'm not a jQuery expert, so I'm not 100% sure this does the same.)
Moreover, decrementing while loops are faster than for loops (reference). If there's no problem with looping through the elements in reverse order, you could rewrite your code to
var i = bots.length;
while (i--) {
…
}
Use offset() or position() depending on if you need coordinates relative to the document or the parent. position() is most likely faster since browsers are efficient at finding offsets relative to the parent. There's no need for parsing the CSS. You also don't need the left and top variables since you only use them once. It may not be as readable but you're going for efficiency:
var left = $("#" + i).position().left + bots[i][1];
var top = $("#" + i).position().top + bots[i][2];
Take a look here for a great comparison of different looping techniques in javascript.
Using for...in has poor performance and isn't recommended on arrays. An alternative to looping backwards and still using a for loop is to cache the length so you don't look it up with each iteration. Something like this:
for(var i, len = bots.length; i < len; i++) { ... }
But there are MANY different ways, as shown in the link above and you might want to test several with your actual application to see what works best in your case.