选择器速度
注意: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 来说:感谢你帮助我们保持警惕 - 我们期待着看到你的库改进,以及每个人都获益。
我一直觉得 jQuery 对 :not 特性的解释很奇怪,尽管我承认我没有阅读规范。在我看来,它应该过滤掉匹配整个选择器的元素,而不是过滤掉匹配选择器任何部分的元素。
我的两分钱。
完全正确,John!
非常感谢你发布这篇文章,因为看到 jQuery 的选择器功能和其他选择器功能的比较总是很高兴。
不过,我必须说,我同意 domQuery 对 :not() 的使用。
我认为 $(“span:not(span.foo)”) 应该返回所有不是 foo 类的 span。
恕我直言,原因是 :not 是一种过滤方法,它应该足够智能,知道如果你已经选择所有 span,那么你就不想忽略所有 span。
这只是我的意见。
此外,恕我直言,我更喜欢使用常规的属性选择器语法,而不是 XPATH 语法,原因是标准。然而,这确实是一个小问题。
至于 domQuery 本身,我一直对可用选项过多感到有点失望,因为它最终会导致简单性方面出现更多问题,但同时,它也为开发人员提供了更多选择,并促进了友好竞争,从而让每个人的优点都得以展现。
总的来说,jQuery 是目前最好的 JavaScript 库,不仅仅是最好的选择器库。jQuery 能够做更多的事情,它真正是一个完整的工具包,最终弥合了样式/格式层(CSS)和行为层(JavaScript)之间的差距。
我真的很期待这个周末!
非常感谢你们的辛勤工作。我已经用我的一个脚本测试了 1.1b 版本,该脚本必须在悬停在一个 td 上时选择一个大型表格中的许多 td(以便对“悬停”的列进行着色,而不仅仅是行)。两个版本之间的速度提升令人惊叹。
我期待着“最终”的 1.1 版本。当然,jQuery 是最好的……
我一直很喜欢这样的事情。对于新版本来说,质量和速度评估可能会很令人兴奋。
关于 :not 语句,我想支持 jQuery。如果你已经指定了你正在过滤 span,那么你不应该在你的查询中包含 span。说我想吃篮子里所有不是苹果且不是红色的苹果没有意义,你只会饿着肚子。
再想想,逻辑更像是:我想吃篮子里所有是苹果且是红色的苹果。这更像是没有必要的冗余,但仍然遵循你不应该在你的查询中包含 span,因为你已经声明你正在过滤 span。
Kyle
jQuery 的实现是说:我想吃篮子里所有不是苹果或不是红色的苹果(没有道理)
domQuery 的实现说的是:我想吃掉篮子里所有不是红苹果的苹果(更合乎逻辑)
你说得对,说“我想吃掉篮子里所有不是红色的苹果”会更有意义。
最重要的是,对于大多数人来说,“div.class” 和 “红苹果” 是同一个意思。因此,他们*期望* domQuery 的版本。
@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 进行比较是有点误导人的...
对于 :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 看起来是最正确的库 :)。
jQuery 非常棒,我是中国人,但我非常喜欢 jQuery。非常感谢。
我不明白为什么 ‘span’ 应该出现在括号中。如果你已经在选择 span,那么指定 class 在 span 下面就是多余的。
关于苹果的比喻,我认为以上例子都没有正确。我会这样理解它们
$(“span:not(.foo)”) : 所有不是红色的苹果
$(“span:not(span.foo)”) : 所有是苹果但不是红色的苹果(多余的)
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 的普通属性选择器中是不可能的(没有 @)。
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) 都是有效的。
John,感谢你的回复。我在我的博客上做了一个回复。
http://www.jackslocum.com/blog/2007/01/12/domquery-in-response-to-jquerys-response/
@Nate C
你完全错了。正如我在原始帖子的结尾所述,DomQuery 只需要一个 getStyle 和 Template 函数。这两个函数只在 1 个地方使用,可以很容易地交换以插入你自己的函数。这两个函数加起来不到 2k,这样总计就会变成 8kb。在发表如此肯定的声明之前,请先做好调查。
John 宣传员,Jack 的回复让你学乖了。他比你聪明,界面也更漂亮,难道这让你很难受吗?哦,还有 Nate,你个白痴。
哇,Greg,你的回复真有礼貌。你用那些胡话为 Jack 赢得了不少分数。顺便说一句,你也批评了 Dean Edwards 吗?还是你只是针对 John 的问题?Dean 也不太高兴,所以我想知道除了故意捣乱之外,你的动机是什么。
@Greg
我们不想看到这种情况。人们互相攻击。
我非常喜欢 Jack 的作品,他新的选择器类使标准提高了许多。这并不意味着 jQuery 不是一个非常好的库。
我希望 jQuery 可以从 Jack 的类中学到一些东西,这样他们的速度可以通过对代码结构进行一些调整而大幅提升。
jquery 万岁!你们如此热衷于这件事真是太棒了。赞赏!
@Tommy:感谢你的评论。Greg 的帖子完全是孩子气,明显是无缘无故地挑起事端。我们肯定会从 Jack 的作品中学习,他用 EXT 做了一些很棒的事情。感谢你体贴的回复,也感谢 Jack 的出色作品。
@Kevin:谢谢兄弟。我们确实对这个项目充满热情,并将继续改进库的各个方面。
回想起来,我现在不同意自己之前的观点,我会这样理解它
span:not(span.foo). 位于具有 ‘foo’ 类别的 span 内部 span
实际上,可能不对。应该是“span > span:not”,对吧?我想我只是把自己搞糊涂了。
唉,这只是一个闹剧,是对这类情况的讽刺。放松一下,别再纠结了,继续你的工作吧。这里没什么好看的,各位。抱歉。
@ 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
(…)
如果我的 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(是和是) = 否
是和否: 不匹配
(…)
我一直很高兴地使用 jQuery,从它早期开始,我一直唯一感到困扰的是 'not'。很长一段时间我一直以为这是 jQuery 的一个 bug(也因为我有一种印象 - 如果我错了请纠正我 - 早期的实现就像 domQuery),直到很晚我才了解到 jQuery 的设计就是这样的。
对我来说,"span.foo" 应该始终匹配所有且仅匹配具有 foo 类的 span 元素,这是 domQuery 的解释。
在 jQuery 中,这在像 $(“span.foo”) 这样的查询中是正确的,但在 .not(“span.foo”) 中则不然。在那里,"span.foo" 匹配所有 span 元素,加上所有具有 foo 类的元素。
这种解读 CSS 选择器的方式让我感到困惑。
顺便说一下,[foo!=bar] 不属于 cssQuery。
精彩的讨论和速度测试。
感谢 Jack 和 John 的努力!
Pingback: Interaction Design Blog » Blog Archive » DomQuery is extremely fast
@Nate Cavanaugh
通过比较 JS 库的大小来证明自己的更小,因此更好,这是荒谬的,是时候明确地说出来了。
如果你真的关心那 2kb 的差异,请比较库的相同子功能,并比较压缩版本,因为代码注释 _是_ 一项功能。
在进行认真的比较之后,你可能会同意,精心编写的代码的大小差异不足以成为选择库的理由。
感谢 Jack 和 John 的出色工作。
DOMQuery 更快。任何吹毛求疵都无法改变这一点。
@Marion
这不是吹毛求疵!这是在讨论这种比较的影响… 开源开发的魅力在于我们互相学习,然后让我们的项目做得更好。在这个过程中,我们讨论问题,学习如何以不同的方式解决相同的问题。Jack 提出了一些显而易见的加速技巧,我很想探索一下。我希望有详细的笔记,包括对编码选择的文档,因为他说过他进行了大量的测试才得出了他选择的实现方式。我很想看看他的测试报告…
$(“apple:not(apple.red)”)
我认为虽然它冗余,但 jQuery 的行为应该将 apple.red 像所有其他选择器一样对待,而不是触发两个独立的过滤器,除非使用逗号。似乎大多数读者都要求改变这种功能,所以我期待着看到它在下一个版本中的发布公告。
XPath @ 在 CSS 选择器中的使用
这可能是个坏主意!实现有限的 CSS 规范并不会限制我们的编码,它只会强制使用 XPath 进行更复杂的查询。这本来就是 XPath 规范的主要目的!如果我们真的想变得疯狂,我们可以使用 jQuery 过滤器方法来进行复杂的筛选。我敢打赌,函数式实现比复杂的筛选字符串更快,因为不需要解析,而且你可以更好地控制迭代逻辑的实现。jQuery 团队必须决定标准合规性是否重要,或者是否支持更多的方式值得违反规范。因为我刚读到一个 jQuery 开发者对 DOMQuery 超出规范的批评,我只能猜测锅说壶黑需要好好清洗了。
关于加速技巧的一点说明
我认为将双引号字符串更改为单引号字符串可能会略微提高速度。在 PHP 和许多其他语言中,双引号会被解析为查找字符串中的变量和转义字符。虽然 JavaScript 不支持字符串中的变量,但它也会解析双引号字符串中的转义字符,而单引号字符串只解析 \’ 作为它解析的唯一值。我不知道我们是在说纳秒还是更重要的东西,但这可能是值得研究的加速技巧。
继续努力吧,各位!你们的热情令人鼓舞。如果我有更多的时间,我很想和你们一起深入研究代码。我现在太忙了,无法贡献,但如果我发现任何值得注意的改进,我会发布到开发者列表。
> 这一点很重要,因为处理重复项的成本非常高(在计算上)。
我认为 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;
}
然后,在表达式引擎运行时,只需简单地将所有结果附加到一个数组中,并在最后清理它。这可能会减少对相同元素的重复运行,从而进一步提高性能。
我会进行一些测试。
嘿 John,我想让你关注这篇文章
http://blog.dojotoolkit.org/2007/02/04/dojoquery-a-css-query-engine-for-dojo
DoJo 又在咬人.. :)
正如 Jack 在那里所说:“我们能否共同努力,创建超快的代码?”
Pingback: DomQuery - 一种轻量级的 CSS 选择器 / 基本 XPath 实现