选择器速度

发布日期 作者

注意:Jack 已经 修复了这篇文章中提到的几乎所有问题 - 干得漂亮!


我们一直没有谈论 jQuery 选择器在 1.1 版本中的速度,直到我们的版本发布临近,但似乎这个过程已经被 加快了。既然这件事已经公开,那么让我们来看看 jQuery 选择器的速度。

简而言之:对于 jQuery 1.1,我们非常努力地让 其选择器变得非常快。事实上,根据我们所有的测试,我们比任何其他选择器库都快。在开发 1.1 版本时,Dean Edwards 的 cssQuery 远远超过了任何其他选择器库。它非常全面,也非常快。

今天,Jack Slocum 宣布了他的新 DOMQuery 选择器库。简而言之:标准提高了。他的库非常快。可能是目前最快的库。

然而,在比较他的库和我们的库时,出现了一些错误,我们想澄清一下。(由 Jack 和 jQuery 共同造成)(作为参考,这是我用于测试的 比较套件。)

jQuery 完全支持所有属性选择器。
例如,[@foo]、[@foo=bar] 等。值得注意的是,jQuery 在这种情况下使用 XPath 风格的语法。由于 Jack 的测试没有考虑到这一点,因此看起来我们所有的属性选择器测试都失败了。

我们的“elem:empty”工作正常。
你可以在 Jack 的测试中看到,所有选择器(除了 DOMQuery)都无法使用 :empty - 这是因为他将结果与 DOMQuery 进行了比较,而 DOMQuery 获取的结果是错误的。规范规定,如果某个元素不包含任何子元素或文本节点,则该元素为空。在这个案例中,似乎没有考虑到这一点。

[foo!=bar]、:first、:last 不属于任何规范。
…然而它们在测试套件中。顺便说一句,jQuery 确实实现了 :first 和 :last - 但没有实现 [foo!=bar](这似乎只存在于 cssQuery 中?)。总之,当你不打算与其他人进行比较时,与其他人进行比较非常奇怪。

span:not(span.hl-code) 匹配什么?
这是一个奇怪的灰色区域,我从未在任何地方看到过讨论,规范也无法对此进行澄清。结果集应该是所有没有 hl-code 类别的 span,还是什么都没有,因为你已经过滤掉了所有 span。例如

// Finds nothing in both
span:not(span)
=> []

// Finds spans that don't have a class of 'foo', in both
span:not(.foo)
=> [ <span>, <span>, ... ]

// jQuery's interpretation of the combination:
$("span:not(span.foo)")
=> []

// DOMQuery's interpretation of the combination:
Ext.select("span:not(span.foo")
=> [ <span>, <span>, ... ]

我们完全承认我们可能在这点上完全错误,但我很好奇听到其他人的意见,以及他们对规范的解释。

DOMQuery 没有考虑重复项。
目前,执行 Ext.select(“div div”) 返回的元素比仅仅执行 Ext.select(“div”) 返回的元素要多 - 并且执行 Ext.select(“div div div”) 返回的元素集又有所不同,但仍然比仅仅执行 Ext.select(“div”) 返回的元素要多。事实上,考虑重复项是 JavaScript 选择器库中一个巨大的问题 - 而目前,jQuery 是唯一一个正确处理这个问题的库。

重点是,考虑重复项可能会非常昂贵(在计算上) - 因此 DOMQuery 不考虑重复项,从而给人一种速度提升的假象。例如

// DOMQuery
Ext.select("div").elements.length
=> 246
Ext.select("div div").elements.length
=> 624
Ext.select("div div div").elements.length
=> 523

// jQuery
jQuery("div").length
=> 246
jQuery("div div").length
=> 243
jQuery("div div div").length
=> 239

DOMQuery 不支持多个过滤器:elem.foo[foo=bar] 或 elem.foo.bar
在实现这一点之前,与任何其他库进行比较都是不公平的。构建一个能够完全处理这些方面的库(例如:cssQuery、jQuery)需要付出巨大的代价。(无论是代码大小还是速度成本。)

DOMQuery 的 #id 选择器不检查上下文
你会注意到,如果你尝试执行类似下面的查询

Ext.select("div #badid").elements
=> [div#badid]

你会得到一个名为“badid”的元素 - 即使该元素实际上并不在 div 内。由于 DOMQuery 代码中没有进行有效性检查,因此它非常快 - 而且非常错误。

我应该提到,在 1.1 版本之前,jQuery 也在这方面犯了错误,因此这是一个很容易忽略的问题。

根元素去哪了?
你会发现,在 DOMQuery 中搜索“html”和“*”奇怪地缺少了一件显而易见的事情:HTML 元素。似乎从所有查询中排除根 DOM 元素有点奇怪;尤其是在以下情况中,这是完全有效的:“html > body *”。

…为了公平起见,这里有一个针对我们自己的:-)

我们的 :nth-child(even/odd) 有缺陷。
目前,它似乎只选择一个元素 (!?)。我为此创建了一个 工单,这个问题应该在本周日发布的 1.1 版本中得到解决。

总的来说,看到 DOMQuery 取得的速度飞跃真是太好了。选择器速度是竞争真正有意义的一个领域;每次速度提高,每个人都受益(用户、开发者 - 每个人)。

事实上,在查看他的代码之后,我已经有一些关于如何提高 jQuery 速度的想法!

所以,对 Jack 来说:感谢你帮助我们保持警惕 - 我们期待着看到你的库改进,以及每个人都获益。

关于“选择器速度”的 33 个想法

  1. Blair Mitchelmore 说:

    我一直觉得 jQuery 对 :not 特性的解释很奇怪,尽管我承认我没有阅读规范。在我看来,它应该过滤掉匹配整个选择器的元素,而不是过滤掉匹配选择器任何部分的元素。

    我的两分钱。

  2. Nate Cavanaugh 说:

    完全正确,John!

    非常感谢你发布这篇文章,因为看到 jQuery 的选择器功能和其他选择器功能的比较总是很高兴。

    不过,我必须说,我同意 domQuery 对 :not() 的使用。
    我认为 $(“span:not(span.foo)”) 应该返回所有不是 foo 类的 span。
    恕我直言,原因是 :not 是一种过滤方法,它应该足够智能,知道如果你已经选择所有 span,那么你就不想忽略所有 span。

    这只是我的意见。

    此外,恕我直言,我更喜欢使用常规的属性选择器语法,而不是 XPATH 语法,原因是标准。然而,这确实是一个小问题。

    至于 domQuery 本身,我一直对可用选项过多感到有点失望,因为它最终会导致简单性方面出现更多问题,但同时,它也为开发人员提供了更多选择,并促进了友好竞争,从而让每个人的优点都得以展现。

    总的来说,jQuery 是目前最好的 JavaScript 库,不仅仅是最好的选择器库。jQuery 能够做更多的事情,它真正是一个完整的工具包,最终弥合了样式/格式层(CSS)和行为层(JavaScript)之间的差距。

    我真的很期待这个周末!

  3. 非常感谢你们的辛勤工作。我已经用我的一个脚本测试了 1.1b 版本,该脚本必须在悬停在一个 td 上时选择一个大型表格中的许多 td(以便对“悬停”的列进行着色,而不仅仅是行)。两个版本之间的速度提升令人惊叹。

    我期待着“最终”的 1.1 版本。当然,jQuery 是最好的……

  4. 我一直很喜欢这样的事情。对于新版本来说,质量和速度评估可能会很令人兴奋。

    关于 :not 语句,我想支持 jQuery。如果你已经指定了你正在过滤 span,那么你不应该在你的查询中包含 span。说我想吃篮子里所有不是苹果且不是红色的苹果没有意义,你只会饿着肚子。

  5. 再想想,逻辑更像是:我想吃篮子里所有是苹果且是红色的苹果。这更像是没有必要的冗余,但仍然遵循你不应该在你的查询中包含 span,因为你已经声明你正在过滤 span。

  6. Kyle

    jQuery 的实现是说:我想吃篮子里所有不是苹果或不是红色的苹果(没有道理)

    domQuery 的实现说的是:我想吃掉篮子里所有不是红苹果的苹果(更合乎逻辑)

    你说得对,说“我想吃掉篮子里所有不是红色的苹果”会更有意义。

    最重要的是,对于大多数人来说,“div.class” 和 “红苹果” 是同一个意思。因此,他们*期望* domQuery 的版本。

  7. Nate Cavanaugh 说:

    @Yehuda:没错!我认为它应该更倾向于人们的预期,尤其是在规范不明确的情况下。

    我还要说,domQuery 与 jQuery 或 domQuery 与 MooTools 的比较是相当误导人的。

    jQuery 和 MooTools 都是轻量级库,它们功能强大,但体积小。

    domQuery 与 Prototype 的 $$ 或 MochiKit 的 $$ 的比较更合适,因为即使 Jack 说他的 domQuery 脚本只有 6k,但它并没有考虑到你必须在里面引用它的大量代码。

    你至少需要以下文件:yahoo.js (4.73kb)、yahoo-dom-event.js(27.3kb)、yui-ext-core.js(53.7kb) 和 domQuery 文件 (6kb)。

    这仅仅是获得选择器表达式所需的最低 91.7kb。

    这就是为什么,在我看来,与那些“其他”框架相比,domQuery 更合理。

    因此,jQuery 不仅在速度和功能上都优于 domQuery,而且它的体积还要小 6 倍。

    Jack 为 YUI 的团队提供了一些非常快速、非常具有表现力的 CSS 选择器表达式,这值得赞扬,但他把 domQuery 与 jQuery 进行比较是有点误导人的...

  8. 对于 :not() 问题,我不得不选择 DOMQuery。CSS 选择器组合形成“AND”条件。因此,span:not(span.foo) 在 JS 语法中是

    if (tagname == ‘span’ && !(tagname == ‘span’ && classname = ‘foo’))

    可以简化为 if (tagname == ‘span’ && classname != ‘foo’)。

    如果你想过滤掉所有 span 和所有具有特定 class 的元素,我想应该是这样的(不确定这是否合法的 CSS)

    span:not(span, .foo)

    其他方面做得很好,是的,jQuery 看起来是最正确的库 :)。

  9. 我不明白为什么 ‘span’ 应该出现在括号中。如果你已经在选择 span,那么指定 class 在 span 下面就是多余的。

    关于苹果的比喻,我认为以上例子都没有正确。我会这样理解它们

    $(“span:not(.foo)”) : 所有不是红色的苹果
    $(“span:not(span.foo)”) : 所有是苹果但不是红色的苹果(多余的)

  10. Jörn Zaefferer 说:

    Steven,我认为最后一个例子应该是

    span:not(span):not(.foo)

    很明显,这没有意义。

    关于属性选择器 (Nate 对它们进行了评论):jQuery 的方括号语法允许不止属性。考虑以下例子

    input[@name=Peter]

    它表示“选择所有名为“Peter”的 input 元素。现在看看这个

    form[input]

    它选择所有包含 input 元素的 form。换句话说:[selector] 语法表示“具有...”。你甚至可以嵌套它们

    form[input[@type=checkbox]]

    选择所有包含 type 为 checkbox 的 input 元素的 form。

    这在 CSS 的普通属性选择器中是不可能的(没有 @)。

  11. span:not(span.foo) 不合法。根据 CSS3 规范

    E:not(s): 不匹配简单选择器 s 的 E 元素

    简单选择器的定义

    简单选择器可以是类型选择器 [p, a, tr 等]、通用选择器 [*]、属性选择器 [[foo]]、类选择器[.foo]、ID 选择器 [#foo]、内容选择器 [?] 或伪类 [:hover]。一个伪元素 [::first-line] 可以附加到最后一个简单选择器序列。

    (方括号中的文本是我自己加的例子。最后一行指的是简单选择器的集合,而不仅仅是一个,就像 :not() 所要求的那样。)

    因此你可以看到,span:not(span.foo) 是无效的,但 span:not(span)、spam:not(.foo) 和 span:not(span):not(.foo) 都是有效的。

  12. Jack Slocum 说:

    John,感谢你的回复。我在我的博客上做了一个回复。

    http://www.jackslocum.com/blog/2007/01/12/domquery-in-response-to-jquerys-response/

    @Nate C
    你完全错了。正如我在原始帖子的结尾所述,DomQuery 只需要一个 getStyle 和 Template 函数。这两个函数只在 1 个地方使用,可以很容易地交换以插入你自己的函数。这两个函数加起来不到 2k,这样总计就会变成 8kb。在发表如此肯定的声明之前,请先做好调查。

  13. Greg Lewis 说:

    John 宣传员,Jack 的回复让你学乖了。他比你聪明,界面也更漂亮,难道这让你很难受吗?哦,还有 Nate,你个白痴。

  14. 哇,Greg,你的回复真有礼貌。你用那些胡话为 Jack 赢得了不少分数。顺便说一句,你也批评了 Dean Edwards 吗?还是你只是针对 John 的问题?Dean 也不太高兴,所以我想知道除了故意捣乱之外,你的动机是什么。

  15. Tommy Maintz 说:

    @Greg

    我们不想看到这种情况。人们互相攻击。
    我非常喜欢 Jack 的作品,他新的选择器类使标准提高了许多。这并不意味着 jQuery 不是一个非常好的库。

    我希望 jQuery 可以从 Jack 的类中学到一些东西,这样他们的速度可以通过对代码结构进行一些调整而大幅提升。

  16. @Tommy:感谢你的评论。Greg 的帖子完全是孩子气,明显是无缘无故地挑起事端。我们肯定会从 Jack 的作品中学习,他用 EXT 做了一些很棒的事情。感谢你体贴的回复,也感谢 Jack 的出色作品。

    @Kevin:谢谢兄弟。我们确实对这个项目充满热情,并将继续改进库的各个方面。

  17. 回想起来,我现在不同意自己之前的观点,我会这样理解它

    span:not(span.foo). 位于具有 ‘foo’ 类别的 span 内部 span

  18. 唉,这只是一个闹剧,是对这类情况的讽刺。放松一下,别再纠结了,继续你的工作吧。这里没什么好看的,各位。抱歉。

  19. @ Jason
    对 :not() 的参数应用于它的主体。主体是指满足选择器和/或组合器[1]的元素。

    因此,span:not(.foo) 指的是所有 span 元素,除了那些具有 'foo' 作为类属性的元素。一旦 "选择引擎" 匹配到满足先前简单选择器(在本例中,所有类型为 'span' 的元素)的元素,引擎就会尝试匹配伪类(在本例中,:not())。

    例如

    a[href].external

    选择引擎会匹配所有类型为 'a' 的元素,然后,对每个元素,引擎会检查它们是否具有名为 'href' 的属性。此时,主体是一个类型为 'a' 的元素,因为它是由先前选择器匹配的。接下来,引擎会尝试在那些具有 'href' 属性的 'a' 元素中查找它们是否也是 'external' 类。这里的主体是匹配 a[href] 的元素。

    如果选择器是 "span:not(span.foo)",并且如果这是一个有效的选择器,那么内部的 span 将引用外部的 span 本身。

    假设你有这样的选择器

    :not(.important)

    这等同于

    *|*:not(.important)

    如果我的 DOM 像这样

    Lorem ipsum

    Dolor sit amet. Consectuor …

    那么想象一下,选择引擎会以这种方式遍历这个 DOM

    元素: .
    匹配 *|*?是
    匹配 .important?否
    :not(否) = 是
    是和是: 匹配

    元素
    匹配 *|*?是
    匹配 .important?否
    :not(否) = 是
    是和是: 匹配

    元素
    匹配 *|*?是
    匹配 .important?否
    :not(否) = 是
    是和是: 匹配

    元素
    匹配 *|*?是
    匹配 .important?是
    :not(是) = 否
    是和否: 不匹配!

    现在想象一下选择器是上面讨论的那个

    span:not(span.foo)

    并假设一个元素

    选择引擎会来到它这里并检查

    元素
    匹配 span?是
    匹配 span?是(来自 span.foo)
    匹配 .foo?是(来自 span.foo)
    :not(是和是) = 否
    是和否: 不匹配

    关于 span:not(span.foo) 是否有意义,根据 Steven 的说法,内部的 span 是多余的,它将匹配所有不是红色的苹果,因为 (A: 苹果,R: 红色)

    A & !(A & R) [ 德摩根定律 ]
    A & (!A | !R) [ 分配律 ]
    ( A & !A ) | ( A & !R ) [ 有界性 ]
    ( 假 ) | ( A & !R ) [ 假或任何东西 -> 任何东西 ]
    ( A & !R )

    所以,所有不是红色的苹果。再仔细想想,这是完全合乎逻辑的,因为当选择引擎评估 :not(span.foo) 时,它已经有一个 span 元素。唯一的麻烦是确定它是否是 'foo' 类。

    http://www.w3.org/TR/2005/WD-css3-selectors-20051215/#subject

  20. (…)

    如果我的 DOM 像这样

    [div]
    [div class=”title”]Lorem ipsum[/div]
    [div class=”text-body”]
    Dolor sit [span class=”important”]amet[/span]. Consectuor …
    [/div]
    [/div]

    那么想象一下,选择引擎会以这种方式遍历这个 DOM

    元素: [div].
    匹配 *|*?是
    匹配 .important?否
    :not(否) = 是
    是和是: 匹配

    元素: [div class=”title”]
    匹配 *|*?是
    匹配 .important?否
    :not(否) = 是
    是和是: 匹配

    元素: [div class=”text-body”]
    匹配 *|*?是
    匹配 .important?否
    :not(否) = 是
    是和是: 匹配

    元素: [span class=”important”]
    匹配 *|*?是
    匹配 .important?是
    :not(是) = 否
    是和否: 不匹配!

    现在想象一下选择器是上面讨论的那个

    span:not(span.foo)

    并假设一个元素 [span class=”foo”]

    选择引擎会来到它这里并检查

    元素: [span class=”foo”]
    匹配 span?是
    匹配 span?是(来自 span.foo)
    匹配 .foo?是(来自 span.foo)
    :not(是和是) = 否
    是和否: 不匹配

    (…)

  21. 我一直很高兴地使用 jQuery,从它早期开始,我一直唯一感到困扰的是 'not'。很长一段时间我一直以为这是 jQuery 的一个 bug(也因为我有一种印象 - 如果我错了请纠正我 - 早期的实现就像 domQuery),直到很晚我才了解到 jQuery 的设计就是这样的。
    对我来说,"span.foo" 应该始终匹配所有且仅匹配具有 foo 类的 span 元素,这是 domQuery 的解释。
    在 jQuery 中,这在像 $(“span.foo”) 这样的查询中是正确的,但在 .not(“span.foo”) 中则不然。在那里,"span.foo" 匹配所有 span 元素,加上所有具有 foo 类的元素。
    这种解读 CSS 选择器的方式让我感到困惑。

  22. Pingback: Interaction Design Blog » Blog Archive » DomQuery is extremely fast

  23. @Nate Cavanaugh
    通过比较 JS 库的大小来证明自己的更小,因此更好,这是荒谬的,是时候明确地说出来了。
    如果你真的关心那 2kb 的差异,请比较库的相同子功能,并比较压缩版本,因为代码注释 _是_ 一项功能。
    在进行认真的比较之后,你可能会同意,精心编写的代码的大小差异不足以成为选择库的理由。

    感谢 Jack 和 John 的出色工作。

  24. Micon Frink 说:

    @Marion
    这不是吹毛求疵!这是在讨论这种比较的影响… 开源开发的魅力在于我们互相学习,然后让我们的项目做得更好。在这个过程中,我们讨论问题,学习如何以不同的方式解决相同的问题。Jack 提出了一些显而易见的加速技巧,我很想探索一下。我希望有详细的笔记,包括对编码选择的文档,因为他说过他进行了大量的测试才得出了他选择的实现方式。我很想看看他的测试报告…

    $(“apple:not(apple.red)”)
    我认为虽然它冗余,但 jQuery 的行为应该将 apple.red 像所有其他选择器一样对待,而不是触发两个独立的过滤器,除非使用逗号。似乎大多数读者都要求改变这种功能,所以我期待着看到它在下一个版本中的发布公告。

    XPath @ 在 CSS 选择器中的使用
    这可能是个坏主意!实现有限的 CSS 规范并不会限制我们的编码,它只会强制使用 XPath 进行更复杂的查询。这本来就是 XPath 规范的主要目的!如果我们真的想变得疯狂,我们可以使用 jQuery 过滤器方法来进行复杂的筛选。我敢打赌,函数式实现比复杂的筛选字符串更快,因为不需要解析,而且你可以更好地控制迭代逻辑的实现。jQuery 团队必须决定标准合规性是否重要,或者是否支持更多的方式值得违反规范。因为我刚读到一个 jQuery 开发者对 DOMQuery 超出规范的批评,我只能猜测锅说壶黑需要好好清洗了。

    关于加速技巧的一点说明
    我认为将双引号字符串更改为单引号字符串可能会略微提高速度。在 PHP 和许多其他语言中,双引号会被解析为查找字符串中的变量和转义字符。虽然 JavaScript 不支持字符串中的变量,但它也会解析双引号字符串中的转义字符,而单引号字符串只解析 \’ 作为它解析的唯一值。我不知道我们是在说纳秒还是更重要的东西,但这可能是值得研究的加速技巧。

    继续努力吧,各位!你们的热情令人鼓舞。如果我有更多的时间,我很想和你们一起深入研究代码。我现在太忙了,无法贡献,但如果我发现任何值得注意的改进,我会发布到开发者列表。

  25. Sebastian Redl 说:

    > 这一点很重要,因为处理重复项的成本非常高(在计算上)。

    我认为 jQuery 中 `merge()` 的朴素实现是造成这种情况的一个主要原因。如果我错了请纠正我,但针对 DOM 节点的专用 `merge()`(选择器引擎所需)可以实现线性时间复杂度,而不是像当前实现一样的二次时间复杂度。

    原因很简单:对象标识。你并不是在寻找具有相同值的物件。你是在寻找被多次引用的相同物件。
    由于物件是相同的,你可以使用一个简单的标记来进行合并。这看起来有点像这样

    function mergeNodes(left, right)
    {
    var r = [];
    var fn = function(e) {
    if(!e.marked) {
    e.marked = true;
    r.push(e);
    }
    };
    jQuery.each(left, fn);
    jQuery.each(right, fn);
    // 清理。
    jQuery.each(r, function(e) { e.marked = false; });
    return r;
    }

    复杂度为 O(n+m) 而不是 O(n*m),其中 n 和 m 分别是两个数组的长度,很明显。

    此外,此解决方案还可以过滤掉源中的重复项。因此你可以这样写

    removeDupes(ar)
    {
    var r = [];
    jQuery.each(ar, function(e) {
    if(!e.marked) {
    e.marked = true;
    r.push(e);
    }
    });
    // 清理。
    jQuery.each(r, function(e) { e.marked = false; });
    return r;
    }

    然后,在表达式引擎运行时,只需简单地将所有结果附加到一个数组中,并在最后清理它。这可能会减少对相同元素的重复运行,从而进一步提高性能。

    我会进行一些测试。

  26. Pingback: DomQuery - 一种轻量级的 CSS 选择器 / 基本 XPath 实现