Sorry if this will sound like a strange need.
Users can create a HTML page with any number of elements in any order, Div's will be assigned a class. I have no control over which order or where they are placed.
I need to traverse the DOM and call an init function on each div with the class names specific to my framework. So I do not know the order they will be placed.
Here are my challenges:
- I am using a recursive .children() Jquery call then iterating through each child of the current element... I think I need to find a faster way of doing this, it is a performance hit.
- When iterating, $.each() is faster than a standard for loop. I have no idea why. Does $.each() perform faster for smaller sets? (i.e. size < 20 )
My other alternative is to do a selector on each class but I need to do several iterations as this can't guarantee the ordering so the iterations cancel out any performance advantage.
So My needs:
- A better performing way to traverse the DOM breadth first. Plain ol' Javascript is fine if I can find better performance than JQuery functions.
Sorry, I know this sounds a bit of a strange need. Any advice is greatly welcome.
First of all, you shouldn't be having a huge performance hit. The issue could somewhere else in your code, but you haven't shown us what you currently have, so I have no way of saying. That said, here's an idea that should be pretty speedy:
var classes = ["class1", "class2", "class3"];
i = classes.length, divs, j, curDiv;
while(i--) {
divs = document.getElementsByClassName(classes[i]);
j = divs.length;
while(j--) {
var curDiv = divs[j];
// call some init function on the current div
}
}
If this isn't fast, you must have an issue somewhere else (also, don't forget to make sure this is called after the document has loaded).
As for whether $.each() is faster than a standard for loop, this is pretty much a rule of thumb: native code is faster than library code (but library code is usually more convenient). That holds true in this case; your average for loop will be much faster than $.each().
Here's something a little bit more concise, with some current syntax.
function bfs(root) {
const queue = [];
while (root) {
console.log(root);
[...root.children].forEach(child => queue.push(child));
root = queue.shift();
}
}
bfs(document.body);
<html>
<head>
<title>Breadth First</title>
</head>
<body>
<div class="a">
<div class="aa">
<span class="aaa"></span>
<span class="aab"></span>
</div>
</div>
<div class="ab">
<span class="aba"></span>
<span class="abb"></span>
</div>
<script src="main.js"></script>
</body>
</html>
For breadth first tree traversal...
const html = `<div class="a">
<div class="aa">
<span class="aaa">
</span>
<span class="aab">
</span>
</div>
<div class="ab">
<span class="aba">
</span>
<span class="abb">
</span>
</div
</div>`;
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
function BFT_dom(root) {
const queue = [];
let currentEl = root;
while(currentEl) {
console.log(currentEl) // Do something to element here
for(let i = 0; i < currentEl.children.length; i++) {
queue.push(currentEl.children[i]);
}
currentEl = queue.shift();
}
}
BFT_dom(doc)
Related
Im trying to add items into a div element based on elements from an array.
The element I'm trying to add already exists on the page. Basically I'm trying to just create a new version of it and add it to the div.
The code might help explain things further.
JavaScript:
function apply(list) {
var item = $("#it_template").children("md-list-item").eq(0);
for(var i = 0; i < list.uuids.length; i++){
var uid = list.uuids[i];
item.children("p").eq(0).html(uid);
$("#items").append(item);
}
}
HTML:
<div id="items">
</div>
<div style="display:none" id="it_template">
<md-list-item md-ink-ripple class="list_item">
<p></p>
</md-list-item>
</div>
It seems to be faulty somewhere, since whenever I'm running the code I'm only seeing one item being added to the div.
Can you please help me out with where the error is?
Try this? The important change is cloning the node instead of trying to append it over and over. (A node can only have one parent, so it will just get moved instead of copied.)
Another change I made was to use .text instead of .html. If you're dealing with text, this is generally much better. (Importantly, it reduces your risk of XSS vulnerabilities.)
function apply(list) {
var item = $("#it_template").children("md-list-item").eq(0);
for(var i = 0; i < list.uuids.length; i++) {
var uid = list.uuids[i];
var newItem = item.clone();
newItem.children("p").eq(0).text(uid);
$("#items").append(newItem);
}
}
Unless you are Using Angular Material, <md-list-item> is an invalid tag.
Sorry, i am not sure what you are attempting to do but if you want to fill something with a collection of somethings and you want to use Angular. Check out NG-REPEAT directive.
var topClick = function() {
child = this.parentNode.parentNode.parentNode.parentNode;
child.parentNode.removeChild(child);
var theFirstChild = document.querySelector(".m-ctt .slt");
data[child.getAttribute("data-id")].rank = 5;
insertBlogs();
};
As you see, there is a part of my code like this:
this.parentNode.parentNode.parentNode.parentNode;
Is there another way to optimize the code (without using jQuery)?
You can use a non recursive helper function, for example:
function nthParent(element, n) {
while(n-- && element)
element = element.parentNode;
return element;
}
You can use recursive helper function, for example:
function getNthParent(elem, n) {
return n === 0 ? elem : getNthParent(elem.parentNode, n - 1);
}
var child = getNthParent(someElement, 4);
An alternative approach
Your goal
According to your comments on the original question, your overall goal is this:
There is a list of blogs.Each blog has a button like "edit" and "delete". When I click such buttons I want to find it's blog element.
I believe the best approach to solve the problem you're facing (as opposed to answering the question you asked - sorry!) is to approach the problem in a different manner.
From your comments, you said you have something like this:
<ul id="blog-list">
<li class="blog-item">
<a href="blog1.com">
Boats galore!
</a>
<span class="blog-description">
This is a blog about some of the best boats from Instagram.
</span>
<span class="blog-button delete-button">
Delete
</span>
<span class="blog-button edit-button">
Edit
</span>
</li>
<li class="blog-item">
<a href="blog2.com">
Goats galore!
</a>
<span class="blog-description">
A blog about the everyday adventures of goats in South Africa.
</span>
<span class="blog-button delete-button">
Delete
</span>
<span class="blog-button edit-button">
Edit
</span>
</li>
<li class="blog-item">
<a class="blog-link" href="blog3.com">
Totes galore!
</a>
<span class="blog-description">
A blog about containers and bags, and the owners who love them.
</span>
<span class="blog-button delete-button">
Delete
</span>
<span class="blog-button edit-button">
Edit
</span>
</li>
</ul>
And your goal is to add click event handlers to the button elements for each blog-link item.
So let's just translate that goal from plain English into pseudo-code:
for each `blog-link` `b`
delete_button.onclick = delete_handler(`b`);
edit_button.onclick = edit_handler(`b`);
Example script
Working example at: http://jsfiddle.net/vbt6bjwy/10/
<script>
function deleteClickHandler(event, parent) {
event.stopPropagation();
parent.remove();
}
function editClickHandler(event, parent) {
event.stopPropagation();
var description = parent.getElementsByClassName("blog-description")[0];
description.innerHTML = prompt("Enter a new description:");
}
function addClickHandlers() {
var bloglistElement = document.getElementById("blog-list");
var blogitems = bloglistElement.getElementsByClassName("blog-item");
blogitems.forEach(function(blogitem){
var deleteButtons = blogitem.getElementsByClassName("delete-button");
deleteButtons.forEach(function(deleteButton){
deleteButton.onclick = function(event) {
return deleteClickHandler(event, blogitem);
}
});
var editButtons = blogitem.getElementsByClassName("edit-button");
editButtons.forEach(function(editButton){
editButton.onclick = function(event) {
return editClickHandler(event, blogitem);
}
});
});
}
HTMLCollection.prototype.forEach = Array.prototype.forEach;
addClickHandlers();
</script>
Explanation
The way you've chosen to implement a solution is valid, but I thought I'd give you a different way to look at the same problem.
In my opinion, the inner tags of the blog entity should not have to have knowledge of the structure or properties of the surrounding HTML for your edit and delete buttons to work.
Your original solution has to work backwards from each button up the chain of parents until it finds what it assumes is the correct parent element, based on a brittle method like hard-coding moving up N times in the chain of parent elements. Wouldn't it be nicer if we could use normal JavaScript element selection to be absolutely sure of what we're selecting? That way, no matter how the HTML structure might change, our JavaScript isn't going to break as long as the classes and IDs remain consistent.
This solution iterates over every blog-item in the #blog-list element:
blogitems.forEach(function(blogitem){ ... });
Within the forEach loop, we grab arrays containing .delete-button and .edit-button elements. On each of those elements, we add the appropriate event handler to the onclick property:
deleteButtons.forEach(function(deleteButton){
deleteButton.onclick = function(event) {
return deleteClickHandler(event, blogitem);
}
});
For each deleteButton element in the array, we assign an anonymous function to the event handler onclick. Creating this anonymous function allows us to create a closure.
This means each deleteButton.onclick function will individually "remember" which blogitem it belongs to.
Check out this SO question/answer about closures for more info: How do JavaScript closures work?
And we throw in the HTMLCollection.prototype.forEach... line to provide forEach functionality to all HTMLCollection objects. Functions like getElementsByClassName return an HTMLCollection object. It acts exactly like an array for the most part, but it doesn't let us use forEach by default. A note about compatibility: you can certainly use a standard for loop, since forEach isn't available in all browsers (mostly old IE browsers are to blame). I prefer the forEach idiom.
End result
The end result is a little bit longer code, since the scope of the problem is a little wider than the question you actually asked. The advantage is that this code is much more flexible and resistant to being broken by changes to the HTML structure.
var topClick = function(event){
console.log(event);
child = this.parentNode.parentNode.parentNode.parentNode;
child.parentNode.removeChild(child);
var theFirstChild = document.querySelector(".m-ctt .slt");
data[child.getAttribute("data-id")].rank = 5;
insertBlogs();
};
Surprise! I print the event through console.log.
And find a array like this inside the event:
And I find the element that I want :
function(event){
console.log(event.path[4]);
child = event.path[4];
}
and it work! How magic! Maybe event agent is another way!
Thank you all the same for giving answers!
Next time before asking questions I'll think over first! :D
I have a
<div id='abc'>
in my html page. This div has thousands of children.
Now when I do the following in JavaScript :
var element = document.getElementById("abc");
element.parentNode.removeChild(element);
This takes 12 seconds in IE11.
Any idea on how to make this go faster ?
I just want to destroy an element in a fast manner.
I also tried with setInnerHtml but this takes just as long...
I don't have IE installed on my mac... but I can suggest that you begin with adding a class that hides it, and only then remove it.
Explanation:
Under the constraints of the single-threaded javascript which means that the browser can use only one core for running javascript, modern browsers will run the CSS and all other calculations on different cores. Maybe after doing that, and maybe a little 'setTimeout', the browser will be more 'prepared' for removing the item.
That's only an educated guess.
You trying to do something like this?
document.getElementById('myButton').onclick = function(){
var myElement = document.getElementById('abc');
myElement.remove()
}
<div id='abc'>
<p>1</p>
<p>1</p>
<p>1</p>
<p>1</p>
</div>
<button id='myButton'>teste</button>
This probably wont speed anything up, but it may at least appear to
var frag = document.createDocumentFragment();
var element = document.getElementById("abc");
// hide the div
element.style.display = 'none';
// doing the following in a setTimeout sometimes made a big difference ... sometimes didn't! IE is tempramental
setTimeout(function() {
frag.appendChild(element); // this
element.parentNode.removeChild(element);
}, 100);
I no longer have IE11, Edge isn't much better at this though, and running the same code gave wildly varying results!! So, try this, I hope it helps in some way
Have you tried using innerHTML to overwrite the content instead of deleting each element? I'm not sure if this works, but it doesn't hurt to give it a try. (The original snippet was made by alexqoliveira)
document.getElementById('myButton').onclick = function(){
var myElement = document.getElementById('abc');
myElement.innerHTML = "";
}
document.getElementById('addElements').onclick = function(){
var elements = "";
for (i = 0; i < 10000; i++) {
elements += "<p>1</p>";
}
var myElement = document.getElementById('abc');
myElement.innerHTML = elements;
}
<div id='abc'>
<p>1</p>
<p>1</p>
<p>1</p>
<p>1</p>
</div>
<button id='addElements'>Add 10000 elements</button>
<button id='myButton'>teste</button>
In terms of performance, is it better to have 20 elements (with their children) in the DOM tree or everything stored in a Javascript object?
Case 1:
<html>
<body>
<div id='el1'>
<span>...</span>
...
</div>
<div id='el2'>
<p>...</p>
...
</div>
<div id='el3'>
<img src="" alt="" />
<br/>
</div>
<div id='el4'>
<section></section>
...
</div>
...
</body>
</html>
Case 2:
var OBJ = {};
OBJ.el1 = $("<div id='el1'><span>...</span>...</div>");
OBJ.el2 = $("<div id='el1'><p>...</p>...</div>");
OBJ.el3 = $("<div id='el1'><img src="" alt="" /><br/>...</div>");
OBJ.el4 = $("<div id='el1'><section></section>...</div>");
....
My application should only show one of these elements at once. Will it perform better if I just leave them in DOM tree and change their style, or is it preferably to store them in a Javascript object and remove and append them when needed?
How could I test it?
#edit
Extra info to consider:
1- all elements were written in the HTML document and then removed and stored in javascript objects when page was loaded.
2- Everytime the active element changes, I have change the DOM structure by removing the current active element and then appending the new one to it.
#EDIT 2: after some tests
The idea is: someone will write the HTML elements to DOM and then Javascript must show/hide only one of these elements at once. These elements have their own children. It may be a Full Page website or a Web App.
I created 2 test cases:
1- Elements will be removed and then re-appended several times by Javascript. So they will be stored in a Javascript object until it's summoned
2- Elements will be in DOM, I'll hide and show them (jQuery functions, but further tests will change the CSS directly) when needed.
<html>
<head>
<script src='https://code.jquery.com/jquery-1.11.3.min.js'></script>
</head>
<body>
<!-- I USED 15 ELEMENTS IN MY TEST, BUT THE CODE IS TOO LONG -->
<script>
var OBJ1 = {};
var OBJ2 = new Array();
var ACTIVE;
var BODY = $(document.body);
var T1, T2, DELTA;
$(function(){
T1 = new Date().getTime();
/*
//TEST 1: Javascript
var _this,id;
$('div').each(function(){
_this = $(this);
id = _this.attr('id');
OBJ1[id] = _this;
_this.remove();
});
var c;
for (var i = 0; i < 10000; i++) {
for(var k in OBJ1){
c = OBJ1[k];
if(ACTIVE) ACTIVE.remove();
BODY.append(c);
ACTIVE = c;
}
}*/
//TEST 2: DOM
var _this,id;
$('div').each(function(){
_this = $(this);
id = _this.attr('id');
OBJ2.push(id);
});
var c;
for (var i = 0; i < 10000; i++) {
for(var k=0,l=OBJ2.length;k<l;k++){
c = $('#'+OBJ2[k]);
if(ACTIVE) ACTIVE.hide();
c.show();
ACTIVE = c;
}
}
T2 = new Date().getTime();
DELTA = T2 - T1;
console.log('It took '+DELTA+' milliseconds');
});
</script>
</body>
I created 5 generic elements with some Lorem Ipsum and Lorem Pixel content and them copied/pasted them 3 times, making 15 1st-level elements.
For 1000 times I hide and revealed each one of the 15 elements. Ran the test 3 times each.
Case 1 (winner so far): 5.2s, 4.75s, 4.85s
Case 2: 21.5s, 21.5s, 20s
#edit 3: changing Case 2 method
Changing the second case from hide()/show() to css() makes a high increase in performance.
//TEST 2: DOM
var _this,id;
$('div').each(function(){
_this = $(this);
_this.css('display','none');
id = _this.attr('id');
OBJ2.push(id);
});
var c;
for (var i = 0; i < 15000; i++) {
for(var k=0,l=OBJ2.length;k<l;k++){
c = $('#'+OBJ2[k]);
if(ACTIVE) ACTIVE.css('display','none');
c.css('display','block');
ACTIVE = c;
}
}
Also increase the loop to 15k.
Case 1: Around 6.5s
Case 2: Around 2.5s
Definitely leave it in the DOM tree. I would just use show/hide methods to show which one you want.
I would only use the JS method if there was a dynamic changing value that should be reflected in the element, but if they are all static, changing the DOM tree is much more performance heavy rather than just a CSS style change.
You can test this by writing both, then using a jsperf to compare.
Javascript is a very fast language, however the DOM is a huge bottleneck. So having the data in a Javascript object will be much faster, but you will have to render the active element to the DOM on your own (Instead of just making it visible).
I wouldn't worry about the DOM being slow for a few dozen elements, but if you have lots of elements or are trying to perform computations/filters on them, definitely go for Javascript.
I think the only dom elements you might want to apply lazy rendering are those that require media content like image, audio, video,etc. For those, search for a lazy loading js library that load the content of that element when you show that element. Also you could leave the element loaded into the page and hidden, but maybe if there are too many media elements it might be a bad use of memory.
Lastly, you could use React.js for this since it's main job is to dinamically render the diff between two dom trees, this might give some great improvements in scenarios where you are rendering the same dom element but with slightly different content. You probably knew about this, is there any reason why you decided to go full vainilla.js on this ?
I have a list of items which diferent event handlers on it.
...
<li>
<div class="item" data-id="1234">
<h3>Item</h3>
<div class="description">...</div>
<ul class="lists">
<li data-list-id="1">Add to list A<li>
<li data-list-id="2">Add to list B<li>
<li data-list-id="3">Add to list C<li>
</ul>
<button class="delete">delete</button>
</div>
</li>
...
Every li under .list has a click event registered which looks like this:
function addToList(event){
var id = event.target.getAttribute('data-id');
var listId = event.target.parentNode.parentNode.getAttribute('data-id');
// XHR stuff
}
There is no problem with this code but the parentNode.parentNode seems really fragile.
For the button would be only one parentNode and for deeper nested elements parentNode^n
I guess this is a common problem and there are more robust solutions?
With jQuery i would use $(target).parentNode('.item')
Whats the best way to do this without jQuery?
As part of my projects, I always write my own toolbox. Sure, I could use jQuery, but I'll stick to my precision toolkit over a sledgehammer, thanks!
With that in mind, have a look at this:
function findParent(source,filter,root) {
root = root || document.documentElement;
while(source != root) {
if( filter(source)) return source;
source = source.parentNode;
}
}
In your case, you can now call:
var listId = findParent(
event.target,
function(e) {return e.attributes && e.attributes['data-id'];}
).getAttribute("data-id");
// note that you should probably break that down, checking if an element
// is found before getting its attribute value... or let the error
// kill your script. Either works.
Now, here it does look a bit more messy than your simple .parentNode.parentNode, but it's much more robust because it doesn't rely on depths being a certain number, which I believe is what you were aiming for.