【问题标题】:Most efficient way to iterate over all DOM elements迭代所有 DOM 元素的最有效方法
【发布时间】:2012-02-03 13:09:06
【问题描述】:

不幸的是,我需要遍历页面的所有 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
}

我假设原生选项更快。想知道是否还有其他我没有考虑过的选择。也许是一个并行迭代子节点的递归选项。

【问题讨论】:

  • 是的,原版 DOM 方式会更快。但是为什么你需要遍历所有元素呢?
  • 缓存你的 items.length 这样你就不会在循环的每次迭代中都计算它,但是是的,带有 DOM 调用的 for 循环将比 .each 更快
  • @jamietre jQuery 也不能很好地优化“body *”。仅使用“body”就可以了,但是如果您使用“body *”,它会使用 sizzle JS。这意味着它最终会调用document.querySelectorAll('body *'),但它必须先运行大约 200 行 Javascript 代码才能决定这样做,包括正则表达式测试和其他事情。这可能看起来不多,但与document.body.getElementsByTagName('*') 相比,它很多。
  • @jamietre 我相信您发现 jQuery 更快的原因是因为您使用的是document.querySelectorAll('*') 而不是document.querySelectorAll('body *')document.body.querySelectorAll('*')。我相应地更新了您的测试:jsperf.com/js-vs-jquery-select-all/2
  • @jamietre 我正在编写一个包含在用户页面中的第三方库。我不知道他们的设计或标记是什么样的。我们的库注入了一个固定元素(标题栏),可能会导致与他们的设计重叠。为了防止这种情况,我们相应地移动它们的任何位置:固定元素。我唯一能假设的是它们可能在 body 元素中有 position:fixed 元素。

标签: javascript jquery performance dom optimization


【解决方案1】:

您发布的 Vanilla Javascript 方式是最快的。它会比您发布的 jQuery 解决方案更快(请参阅我对问题的评论)。如果您没有在循环中删除或添加任何内容到 DOM 并且遍历顺序无关紧要,您也可以通过反向迭代来稍微加快它:

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

编辑:检查此基准以了解使用本机代码可以节省多少时间:http://jsben.ch/#/Ro9H6

【讨论】:

  • 我认为您省略了 for 循环的条件部分,但看起来代码实际上将永远运行,使用 i-- 作为不递减的条件。对吗?
  • @BrianJ i-- 是循环的条件部分。这就像说i-- != 0,但由于0falsy,你可以不考虑这部分
  • var i = 0, items = startElem.getElementsByTagName("*"); while(items[i]) {//dostuf ;我++ }
  • 我很好奇为什么向后迭代会更快。你能解释或指出一篇文章吗?
  • @Lamar 我认为它不再更快了,因为 JavaScript 引擎现在在优化方面做得很好。很久以前,它稍微快一点,因为不是在每次迭代时都执行i++,然后不使用该表达式的结果,而是单独评估i &lt; length 的循环条件,您可以使用i--;(实际上是@ 987654330@) 既可以推进迭代,也可以作为循环条件。
【解决方案2】:

这通常不是一个好主意,但这应该可行:

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;
}

已编辑!

【讨论】:

  • 我采用了这种方法并将其与下面的一些基准测试集成。它似乎要快得多。我错过了什么吗? jsfiddle.net/Pv8zm/7
  • 能否更新此示例以不包含文本节点?递归很有趣,但它迭代了不必要的节点
  • 我认为真正的问题在于检查每个节点的计算 CSS,而不是来自选择器。但是要跳过文本节点,只需使用 firstElementChildnextElementSibling 代替。一旦添加了其余的逻辑,我认为这与使用选择器不会有实质性的不同(例如,这不是让它变慢的原因)。
【解决方案3】:

更新:

不要使用$('body *') 来迭代元素。如果您使用 JQuery 方法,使用 $('*') 会快得多(有关详细信息,请参阅 cmets)。


Plain ol' JavaScript 相对来说要快得多。

使用test fiddle,我用 JQuery 处理 13000 个元素大约需要 30 毫秒,使用 JavaScript 处理 23000 个元素需要 8 毫秒(均在 Chrome 上测试):

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

Difference:  664% in favor of plain ol' JavaScript

注意:除非您的页面上有非常多的元素,否则这不会产生太大影响。此外,您可能应该对循环中的逻辑进行计时,因为这可能是所有这一切的限制因素。

更新:

Here 是考虑更多元素(每个循环大约 6500 个)时的更新结果,使用 JQuery 在 1500 毫秒内我得到大约 648000 个元素,使用 JavaScript 在 170 毫秒内得到大约 658000 个元素。 (均在 Chrome 上测试):

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

Difference:  895% in favor of plain ol' JavaScript

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

【讨论】:

  • 这是一个非常不公平的测试,因为您只迭代了少数元素。当然 jQuery 会被杀死,因为调用 any 方法有很大的开销。随着元素数量的增加,差异将减少到几乎没有。 (参见 cmets 中链接到的 jsperf 测试 user1 和我自己)
  • 你正在 jQuery 循环中做一些你在另一个循环中没有的事情:将每个元素转换为一个 jQuery 对象var $this = $(this);。使两个迭代器相同,我想您会发现这种差异在很大程度上消失了。
  • 不,不是。 “this”是一个已经在循环中的元素......就像本机方法返回的元素一样。为什么在一种情况下必须将其转换为 jQuery 对象,而在另一种情况下则不需要?他们一开始是一样的。
  • 这是一个基于你的更公平的测试(对两者使用相同的选择器,并且使用相同的迭代方法):jsfiddle.net/Pv8zm/4
  • @kevzettler:我认为该方法也会迭代文本节点,因此它增加了处理的节点数量。见this test
【解决方案4】:

这是 cmets 中描述的问题的解决方案(尽管不是实际问题)。我认为使用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});

【讨论】:

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

最快的方法似乎是document.all(注意它是一个属性,而不是一个方法)。

我已经修改了 Briguy 的答案以记录这些而不是 jQuery,并且它始终更快(比 document.getElementsByTagName('*'))。

The fiddle.

【讨论】:

【解决方案6】:

最高效:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-19
    • 2019-02-10
    • 2018-01-03
    • 2012-09-27
    • 1970-01-01
    相关资源
    最近更新 更多