15

基准比较 QSA & .forEachvs aNodeIterator

toArray(document.querySelectorAll("div > a.klass")).forEach(function (node) {
  // do something with node
});

var filter = {
    acceptNode: function (node) {
        var condition = node.parentNode.tagName === "DIV" &&
            node.classList.contains("klass") &&
            node.tagName === "A";

        return condition ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
    }  
}
// FIREFOX Y U SUCK
var iter = document.createNodeIterator(document, NodeFilter.SHOW_ELEMENT, filter, false);
var node;
while (node = iter.nextNode()) {
    // do thing with node    
}

现在要么NodeIterator很烂,要么我做错了。

问题:我应该什么时候使用NodeIterator?

如果您不知道,DOM4 指定了NodeIterator是什么。

4

2 回答 2

32

NodeIterator(并且TreeWalker,就此而言)几乎从未使用过,原因有很多。这意味着有关该主题的信息很少,并且出现了像 @gsnedders 之类的答案,这完全不符合标准。我知道这个问题已经有将近十年的历史了,所以请原谅我的死灵法。

  1. 启动和性能 = 确实 a 的启动NodeIterator类似的方法慢得多querySelectorAll,但这不是您应该衡量的性能。

关于 s 的事情NodeIterator是它们是活的,就像 anHTMLCollection或 live一样NodeList,您可以在启动一次后继续使用该对象。返回的
是静态的,每次需要匹配新添加的元素时都必须重新启动。NodeListquerySelectorAll

这个版本的 jsPerf 放入NodeIterator了编写代码。实际测试仅尝试使用 . 遍历所有新添加的元素iter.nextNode()。您可以看到迭代器现在快了几个数量级。

  1. 选择器性能 = 好的,很酷。缓存迭代器更快。但是,此版本显示了另一个显着差异。我添加了 10 个done[0-9]选择器不应该匹配的类 ( )。迭代器损失了大约10%的速度,而 querySelectors 损失了20%

另一方面,这个版本显示了当您div >在选择器的开头添加另一个时会发生什么。迭代器损失了33%的速度,而 querySelectors 的速度增加10%

在这个版本中删除div >选择器开头的首字母表明这两种方法都变得更慢,因为它们比早期版本匹配得更多。正如预期的那样,在这种情况下,迭代器的性能相对比 querySelectors 更高。

这意味着基于节点自身属性(其类、属性等)的过滤可能在 a 中更快NodeIterator,而在选择器中有很多组合符(>、+、~ 等)可能意味着querySelectorAll更快。 对于(空间)组合
器尤其如此。 选择带有 的元素querySelectorAll('article a')比手动遍历每个a元素的所有父元素要容易得多,寻找一个tagName具有'ARTICLE'.

PS 在第 3.2 节中,我举了一个例子,如果你想要与空间组合器的作用相反(排除 a带有article祖先的标签),那么完全相反的情况是如何发生的。

3 不可能的选择器

3.1 简单的层次关系

当然,手动过滤元素可以为您提供几乎无限的控制。这意味着您可以过滤掉通常无法与 CSS 选择器匹配的元素。例如,CSS 选择器只能“回看” div因为可以div使用div + div. 选择紧随其后divs是不可能的。div

但是,在 aNodeFilter中,您可以通过检查来实现这一点node.nextElementSibling.tagName === 'DIV'。CSS 选择器无法做出的每一个选择也是如此。

3.2 更多的全局层次关系

我个人喜欢使用NodeFilters 的另一件事是,当传递给 a 时,您可以通过返回而不是返回来TreeWalker拒绝节点及其整个子树。NodeFilter.FILTER_REJECTNodeFilter.FILTER_SKIP

想象一下,您想要遍历页面上的所有标签,但有祖先a的标签除外。使用 querySelectors,您可以输入类似article

let a = document.querySelectorAll('a')
a = Array.prototype.filter.call(a, function (node) {
  while (node = node.parentElement) if (node.tagName === 'ARTICLE') return false
  return true
})

在 a 中NodeFilter,您只需要输入这个

return node.tagName === 'ARTICLE' ? NodeFilter.FILTER_REJECT : // ✨ Magic happens here ✨
       node.tagName === 'A'       ? NodeFilter.FILTER_ACCEPT :
                                    NodeFilter.FILTER_SKIP

综上所述

您不必在每次需要迭代相同类型的节点时都启动 API。可悲的是,这个假设是在提出问题时做出的,+500 的答案(给予它更多的功劳)甚至没有解决错误或任何好处NodeIterator

NodeIterator必须提供两个主要优势:

  • 如第 1 节所述,生动活泼
  • 高级过滤,如 §3 中所述
    (我不能强调这个NodeFilter.FILTER_REJECT示例的有用性)

但是,NodeIterator当满足以下任一条件时,请勿使用 s:

  • 它的实例只会被使用一次/几次
  • 使用 CSS 选择器查询复杂的层次关系
    (即body.no-js article > div > div a[href^="/"]

对不起,答案很长:)
于 2019-10-03T14:45:46.550 回答
14

由于各种原因,它很慢。最明显的事实是没有人使用它,所以花在优化它上的时间要少得多。另一个问题是它是大量可重入的,每个节点都必须调用 JS 并运行过滤器功能。

如果您查看基准测试的修订版 3,您会发现我添加了迭代器正在使用的功能的重新实现,getElementsByTagName("*")然后在其上运行相同的过滤器。结果显示,它的速度要快得多。使用 JS -> C++ -> JS 很慢。

getElementsByTagName完全在 JS(案例)或 C++(案例)中过滤节点querySelectorAll比通过重复越界进行过滤要快得多。

另请注意,由 使用的选择器匹配querySelectorAll相对智能:它进行从右到左的匹配并且基于预先计算的缓存(大多数浏览器将遍历所有具有类“klass”的元素的缓存列表,检查是否它是一个a元素,然后检查父元素是否是 a div),因此他们甚至不会费心遍历整个文档。

鉴于此,何时使用 NodeIterator?至少在 JavaScript 中基本上从来没有。在诸如 Java 之类的语言中(无疑是有一个称为 NodeIterator 的接口的主要原因),它可能和其他任何语言一样快,因为您的过滤器将使用与过滤器相同的语言。除此之外,唯一有意义的其他时间是在创建 Node 对象的内存使用量远远大于 Node 的内部表示的语言中。

于 2011-10-30T14:14:48.917 回答