遍历所有DOM元素的最有效方式

67

很不幸,我需要迭代一个页面上的所有DOM元素,想知道最有效的技术是什么。我可能可以自己进行基准测试,如果有时间可能会这样做,但我希望有人已经经历过这个或者有一些我没有考虑过的选项。

目前我正在使用jQuery并执行以下操作:

$('body *').each(function(){                                                                                                                            
    var $this = $(this);                                                                                                                                
    // do stuff                                                                                                                                         
});

虽然它能够工作,但似乎会导致客户端出现一些滞后。它也可以通过更具体的jQuery上下文进行调整,例如$('body', '*')。我想到原生JavaScript通常比jQuery更快,我找到了这个:

var items = document.getElementsByTagName("*");
for (var i = 0; i < items.length; i++) {
    // do stuff
}

我假设使用本地选项更快。想知道是否有其他选项我没考虑到的。也许有一个递归选项,可以并行迭代子节点。


2
是的,使用原始的DOM方式会更快。但是为什么您需要迭代遍历“所有”元素呢? - Matt Ball
7
为了提高效率,可以缓存items.length的值,避免在每次循环迭代时都重新计算。同时需要注意,用for循环加DOM调用的速度会比使用.each更快。 - kinakuta
5
@jamietre,jQuery 也不能很好地优化 "body *"。 它可以很好地处理 "body",但是如果使用 "body *",它将使用 sizzle JS。 这意味着它最终会调用 document.querySelectorAll('body *'),但它必须先运行大约 200 行的 Javascript 代码,包括一个正则表达式测试和其他内容,然后才决定这样做。 这可能看起来不像什么,但与 document.body.getElementsByTagName('*') 相比,它确实是很多的。 - Paul
3
我相信你认为jQuery更快的原因是因为你使用了document.querySelectorAll('*')而不是document.querySelectorAll('body *')或者document.body.querySelectorAll('*')。我已经更新了你的测试:http://jsperf.com/js-vs-jquery-select-all/2。 - Paul
2
@jamietre 我正在编写一个第三方库,该库将包含在用户页面中。我不知道他们的设计或标记是什么样子的。我们的库将注入一个固定元素(标题栏),这可能会与他们的设计重叠。为了防止这种情况发生,我们会相应地移动任何他们的position:fixed元素。唯一可以假设的事情是,他们可能会在body元素内有position:fixed元素。 - kevzettler
显示剩余15条评论
6个回答

51

你发的这个原生JavaScript的方式是最快的。它比你发的jQuery解决方案更快(请看我在问题下的评论)。如果你在循环中不会添加或者移除DOM元素,并且遍历顺序不重要,那么你可以通过反向迭代来稍微加速:

var items = startElem.getElementsByTagName("*");
for (var i = items.length; i--;) {
    //do stuff
}

编辑:查看这个基准测试以了解使用本地代码可以节省多少时间:http://jsben.ch/#/Ro9H6


我认为你遗漏了for循环的条件部分,但看起来代码实际上会一直运行下去,使用i--作为条件而没有递减。这是正确的吗? - Brian J
15
@BrianJ,“i--”是循环的条件部分。这就像说“i-- != 0”,但由于“0”是“falsy”,所以可以省略该部分。 - Paul
var i = 0, items = startElem.getElementsByTagName("*"); while(items[i]) {//做一些事情 ; i++ } - Yuvaraj V
我很好奇为什么反向迭代会更快。你能解释一下或者指出一篇文章吗? - Lamar
@Lamar,我认为现在它已经不再更快了,因为JavaScript引擎现在已经非常擅长优化了。很久以前,它略微快一些,因为在每次迭代中不使用该表达式的结果而是单独评估“i < length”来进行循环条件,你可以使用“i--;”(有效地是“i-- != 0”)来推进迭代并作为循环条件。 - Paul
@Paulpro 我认为主要原因与本地变量的数量有关。递减迭代器更快,因为在向前迭代涉及两个变量(一个用于长度,一个用于索引)的情况下,在向后迭代中只涉及一个变量:当前索引。但是,如果Javascript像C/C++一样具有内存指针,则从数组的起始内存地址到结束地址进行正向迭代应该比向后迭代更快,因为您不必在每次迭代中将起始地址添加到索引计数器中。 - Jack G

16

更新:

不要使用$('body *')来遍历元素。如果您选择使用JQuery方法(详见注释),使用$('*')会更快。


相对而言,普通的JavaScript更快。

使用测试页面,在Chrome上测试,处理13000个元素使用JQuery需要约30ms,而使用JavaScript处理23000个元素只需要8ms:

JQuery:      433  elements/ms
JavaScript:  2875 elements/ms

Difference:  664% in favor of plain ol' JavaScript

注意:除非您的页面上有非常大量的元素,否则这不会产生太大的差异。此外,您可能应该计算循环中的逻辑时间,因为这可能是所有问题的限制因素。

更新:

这里是在考虑更多元素时(大约每个循环中有6500个元素)的最新结果:使用JQuery时,在1500ms内获得约648,000个元素,在170ms内使用JavaScript获得658,000个元素。(两者都在Chrome上进行了测试)

JQuery:      432  elements/ms
JavaScript:  3870 elements/ms

Difference:  895% in favor of plain ol' JavaScript

看起来 JavaScript 的速度加快了,而 JQuery 的速度保持不变。


@jamietre:好的,请稍等,我将增加元素的数量。 - Briguy37
3
你在jQuery循环中做了一些其他循环中没有的事情:将每个元素转换为jQuery对象var $this = $(this);。使这两个迭代器相同,我认为你会发现这个差异大部分消失了。 - Jamie Treworgy
2
不,不是这样的。"this"已经是循环中的一个元素了...就像本地方法返回的那个一样。为什么在一种情况下你必须将它转换为jQuery对象,而在另一种情况下不需要呢?它们最初是相同的。 - Jamie Treworgy
2
这里有一个更公平的测试,基于你的测试(使用相同的选择器和迭代方法):http://jsfiddle.net/Pv8zm/4/ - Jamie Treworgy
2
@kevzettler:我认为该方法也会迭代文本节点,因此它会增加已处理节点的数量。请参见此测试 - Briguy37
显示剩余6条评论

15

一般来说这不是一个好主意,但是这应该能够起作用:

function walkDOM(main) {
    var arr = [];
    var loop = function(main) {
        do {
            arr.push(main);
            if(main.hasChildNodes())
                loop(main.firstChild);
        }
        while (main = main.nextSibling);
    }
    loop(main);
    return arr;
}
walkDOM(document.body);

不包括文本节点:

function walkDOM(main) {
    var arr = [];
    var loop = function(main) {
        do {
            if(main.nodeType == 1)
                arr.push(main);
            if(main.hasChildNodes())
                loop(main.firstChild);
        }
        while (main = main.nextSibling);
    }
    loop(main);
    return arr;
}

已编辑!


2
我已经采用了这种方法并将其与下面的一些基准进行了整合。 它似乎要快得多。 我是否遗漏了什么? http://jsfiddle.net/Pv8zm/7/ - kevzettler
1
这个例子能否更新,不包括文本节点?递归很有趣,但它正在迭代不必要的节点。 - kevzettler
我认为真正的时间消耗将来自于检查每个节点的计算CSS,而不是选择器。但是要跳过文本节点,只需使用firstElementChildnextElementSibling即可。我认为,一旦添加了其余的逻辑(例如,这不是使其变慢的原因),这与仅使用选择器没有实质性区别。 - Jamie Treworgy

7
最快的方法似乎是document.all(注意它是一个属性,而不是一个方法)。
我已经修改了Briguy答案的fiddle,将其记录下来而不是使用jQuery,并且它始终比document.getElementsByTagName('*')更快。 这个fiddle

不幸的是,这不是一个标准属性:https://developer.mozilla.org/zh-CN/docs/Mozilla/Mozilla_Web_Developer_FAQ#JavaScript_doesn.E2.80.99t_work.21_Why.3F - hoju

3
这是对评论中描述问题的解决方案(虽然不是实际问题)。我认为,使用elementFromPoint测试您想要放置固定位置元素的区域,并仅关注该区域内的元素会更快。这里有一个示例: http://jsfiddle.net/pQgwE/4/ 基本上,只需设置一些最小可能的要查找的元素大小,并扫描您的新固定位置元素想要占用的整个区域。构建在那里发现的唯一元素列表,并仅关注检查这些元素的样式。
请注意,此技术假定您要查找的元素具有最高的z-index(对于固定位置似乎是合理的假设)。如果这不够好,则可以调整为在发现每个元素后隐藏它们(或分配最小z-index),并再次测试该点,直到不再发现任何东西(以确保),然后再恢复它们。这应该发生得非常快,以至于无法察觉。
HTML:
<div style="position:fixed; left: 10px; top: 10px; background-color: #000000; 
    color: #FF0000;">I Am Fixed</div>
<div id="floater">OccupyJSFiddle!<br>for two lines</div>

JS:

var w = $(window).width(), h=$(window).height(),
    minWidth=10,
    minHeight=10, x,y;

var newFloat = $('#floater'), 
    maxHeight = newFloat.height(),
    el, 
    uniqueEls=[],
    i;

for (x=0;x<w;x+=minWidth) {
    for (y=0;y<h&& y<maxHeight;y+=minHeight) {
        el = document.elementFromPoint(x,y);
        if (el && $.inArray(el,uniqueEls)<0) {
            uniqueEls.push(el);
        }
    }
}
// just for the fiddle so you can see the position of the elements 
// before anything's done
// alert("click OK to move the floater into position.");
for (i=0;i<uniqueEls.length;i++) {
    el = $(uniqueEls[i]);
    if (el.css("position")==="fixed") {
        el.css("top",maxHeight+1);
    }
}

newFloat.css({'position': 'fixed',
             'top': 0,
             'left': 0});

1
谢谢你的尝试。然而我不认为它适用于我的情况。我已经更新了你的示例,使其更类似于我在生产中看到的情况。http://jsfiddle.net/pQgwE/5/ 两个固定定位的元素在顶部:0像素重叠。此外,在我这种情况下,外部设计中有多个固定元素。其中一些相对于其他元素定位,所以使用elementFromPoint方法可能会很快失控。 - kevzettler
1
除非您有两个实际上占用完全相同的空间的重叠元素,否则它仍然可以工作,参见:http://jsfiddle.net/pQgwE/14/ -- 您所做的更改将原本应该是您的元素变成了其他东西。但是,如果由于某种原因您希望您的客户在完全相同的空间中拥有多个固定位置元素(这毫无意义,因为它们会互相干扰),那么您可以像我上面建议的那样做:在检测到每个元素后,隐藏它或降低其z-index,并再次检查看看它后面是什么。 - Jamie Treworgy
顺便说一句,我不确定你所说的“elementFromPoint方法可能会很快失控”的意思。无论元素是相对定位还是其他任何方式,都没有关系。这就是该方法的优点所在。它告诉你特定点上的内容,而不管它是如何到达那里的。一旦检测到需要移动的元素,使用此方法所做的事情与通过整个DOM进行迭代查找它们时所做的完全相同。只是用这种方式找到感兴趣的东西要少得多。 - Jamie Treworgy
2
抱歉,我又尝试了一下您的示例,它似乎正好符合我的需求。我已更新示例以展示我正在使用的布局类型,它比我所希望的要更流畅。http://jsfiddle.net/pQgwE/17/ - kevzettler
3
听起来很不错。调整这个逻辑以处理不同情况应该很容易 - 如果您只有一个标题栏大小的区域要检查,即使每隔一个像素进行检查(如果必须),也比迭代/检查整个DOM中计算CSS要快得多。 但我打赌每10像素就足够了,而且应该几乎是瞬间完成的。如果您需要帮助调整,请让我知道。 - Jamie Treworgy

1

Most efficient:

const allDom = document.all || document.querySelectorAll("*");
const len = allDom.length;
for(let i=0; i<len; i++){
    let a = allDom[i];
}

https://jsben.ch/FwPzW


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接