为什么“this”比保存的选择器更有效?

18
我在做这个测试用例,以查看使用this选择器能加快多少进程。在进行测试时,我决定尝试预先保存的元素变量,认为它们会更快。但是,在测试之前保存元素变量似乎是最慢的,这让我很困惑。我认为只需要“查找”元素一次就可以极大地加快进程。为什么不是这种情况呢?
以下是我的测试结果,从最快到最慢,以防有人无法加载它:
1
$("#bar").click(function(){
    $(this).width($(this).width()+100);
});
$("#bar").trigger( "click" );

2

$("#bar").click(function(){
    $("#bar").width($("#bar").width()+100);
});
$("#bar").trigger( "click" );

3

var bar = $("#bar");
bar.click(function(){
    bar.width(bar.width()+100);
});
bar.trigger( "click" );

4

par.click(function(){
    par.width(par.width()+100);
});
par.trigger( "click" );

我本以为顺序应该是4,3,1,2,按照需要使用选择器来“查找”变量的频率排序。

更新:我有一个理论,但我希望有人能验证一下。我猜想在点击时,它必须引用变量,而不仅仅是元素,这会减慢速度。


1
而且更快(并且免受测试的副作用影响):(document.getElementById('bar').onclick = function() {this.style.width = this.offsetWidth+10+"px";})(); - Niet the Dark Absol
5个回答

9

固定测试用例: http://jsperf.com/this-vs-thatjames/10

简而言之:每个测试中执行的单击处理程序数量增加,因为元素在测试之间未重置。

测试微优化的最大问题在于您必须非常小心地测试什么。有许多情况下,测试代码会干扰您正在测试的内容。这是Vyacheslav Egorov的示例,该示例“证明”了JavaScript中乘法几乎是瞬间完成的,因为JavaScript编译器完全删除了测试循环:

// I am using Benchmark.js API as if I would run it in the d8.
Benchmark.prototype.setup = function() {
  function multiply(x,y) {
    return x*y;
  }
};

var suite = new Benchmark.Suite;
suite.add('multiply', function() {
  var a = Math.round(Math.random()*100),
      b = Math.round(Math.random()*100);

  for(var i = 0; i < 10000; i++) {
     multiply(a,b);
  }
})

既然您已经意识到有一些违反直觉的事情正在发生,那么您应该特别小心。

首先,您没有在测试选择器。您的测试代码执行了:零个或多个选择器(取决于测试),一个函数创建(在某些情况下是闭包,而在其他情况下不是),分配为点击处理程序和触发jQuery事件系统。

此外,您正在测试的元素在测试之间发生了变化。很明显,一个测试中的宽度比之前的测试中的宽度更大。但这并不是最大的问题。问题是一个测试中的元素具有X个关联的点击处理程序。下一个测试中的元素具有X+1个点击处理程序。因此,当您触发最后一个测试的点击处理程序时,也会触发所有之前测试中关联的点击处理程序,使测试速度比之前慢得多。

我已经修复了jsPerf,但请记住,它仍然没有仅测试选择器性能。尽管如此,最重要的影响结果的因素已被消除。


注意:有一些幻灯片和一个视频关于如何使用jsPerf进行良好的性能测试,重点是应该避免的常见陷阱。主要思路:

  • 不要在测试中定义函数,而应该在设置/准备阶段中进行
  • 尽可能保持测试代码简单
  • 比较做同样事情的东西或者提前说明
  • 测试你打算测试的内容,而不是设置代码
  • 隔离测试,在每个测试之后/之前重置状态
  • 没有随机性。如果需要,模拟它
  • 注意浏览器优化(死代码删除等)

哦,哇,我以为 jsperf 会以某种方式重置所有内容。我非常无知它是如何工作的。我假设它在单独的模块中运行所有内容或在测试之间自动重置所有内容。 - James G.

6
你并没有真正测试不同技术之间的性能。
如果你查看修改后的测试的控制台输出:http://jsperf.com/this-vs-thatjames/8,你会发现有多少个事件侦听器附加到#bar对象上。你还会发现它们在每个测试开始时没有被移除。因此,随着先前的测试需要调用所有先前的回调函数,所以下面的测试将始终变得更慢。

当我编写基准测试时,我经常编写一个循环,至少运行所有基准测试两次,以考虑可能会使第一次运行比正常情况更或更少有效的奇怪影响(例如页面故障、磁盘缓存等)。看到一个实际错误被我的基准测试发现,这种做法很好! - user1084944
@Hurkyl 编写好的测试是一件痛苦的事情。你需要考虑很多因素,这样做错误的可能性比创建有意义的测试更大。你不仅需要完全了解你的代码,还需要了解涉及到的API的行为。而且你需要确定测试的方式是否反映了真实的场景。例如,如果一个函数只有在每秒至少调用x次时才能更好地执行,在真实的场景中它最多只能被调用y次(其中y<x),那么这样的压力测试也会误导人。 - t.niese

3

部分减慢的原因是对象引用已经在内存中找到,因此编译器不必在内存中寻找变量。

$("#bar").click(function(){         
    $(this).width($(this).width()+100); // Only has to check the function call
});                                     // each time, not search the whole memory

与之相反
var bar = $("#bar");
...
bar.click(function(){
    bar.width(bar.width()+100);         // Has to search the memory to find it 
});                                     // each time it is used

正如zerkms所说,解引用(即像我上面描述的查找内存引用)对性能有一些影响,但影响很小。

因此,你进行的测试中差异的主要原因是DOM在每个函数调用之间没有重置。实际上,保存的选择器的执行速度几乎与this一样快。


1
@mpm 我相信是这样的,但我可能在这个事实上错了。也许它只搜索变量一次,我不确定。 - Zach Saucier
1
我不相信变量解引用是那么昂贵的 http://jsperf.com/function-call-vs-closure - zerkms
正如您所看到的 - 变量查找与函数调用相比是瞬间完成的。 - zerkms
我很好奇为什么这个被踩了 - 能否请这位朋友解释一下为什么这个不是有用的呢? - Zach Saucier
不考虑任何贬值,但是这个解释的大部分并不是正确答案。请阅读@t.niese的答案,他解释了问题所在。性能测试中的代码并没有测试它应该测试的内容,而是建立了大量的点击处理程序。您可以通过编辑原始测试并在第一个和第三个测试之间交换代码来轻松测试它,并且会发现与原始测试不同的结果。 - Robert Westerlund
显示剩余7条评论

2

看起来你得到的性能结果与代码无关。如果你查看这些编辑过的测试,你会发现在两个测试中有相同的代码(第一个和最后一个),但结果完全不同。


-4

我不确定,但如果必须猜测的话,我会说这是由于并发和多线程引起的。

当你执行$(...)时,你调用了jQuery构造函数并创建了一个新对象,该对象被存储在内存中。然而,当你引用现有变量时,你不会创建一个新对象(显然)。

虽然我没有引用的来源,但我相信每个JavaScript事件都在自己的线程中调用,因此事件不会互相干扰。按照这种逻辑,编译器必须锁定变量才能使用它,这可能需要时间。

再次强调,我不确定。非常有趣的测试!


2
除了工作者(workers)之外,所有的JavaScript代码都由同一个线程执行,在称为事件循环的东西中。您获取一个事件,执行与其关联的每个处理程序,然后获取下一个事件。 - Tibos
@Tibos 所以按照这个逻辑,如果一个事件处理程序或其他代码片段需要很长时间,它会锁定吗? - bmendric

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