jQuery 的批评者对此有两个合理的抱怨。第一个抱怨是 jQuery 创建了难以阅读的意大利面代码。在上一章中,我们通过展示如何编写既易于阅读又易于维护的代码来解决这个问题。第二个抱怨是 jQuery 创建的代码太慢。这也是一个合理的投诉。jQuery 或任何其他库的问题在于,如果您不理解它,很容易选择错误的方法来做某事。在本章中,我们将解决第二个问题:jQuery 代码太慢。
可以肯定的是,jQuery 本身是用高性能优化的 JavaScript 编写的;事实上,我强烈建议您研究它的源代码。性能和 jQuery 的问题通常是不理解它是如何工作的。正是这种理解的缺乏导致程序员编写效率低下的代码。但谢天谢地,jQuery 并不难理解,当我们将这种理解与性能度量工具结合起来时,我们可以轻松地提高代码的性能。
在本章中,我们将:
- 了解如何测量 JavaScript 代码的速度
- 测量不同 jQuery 代码段的性能
- 了解何时不使用 jQuery,而使用普通 JavaScript
在我们担心如何提高应用程序的性能之前,我们应该先了解如何衡量它。仅仅说“应用程序感觉迟钝”是不够的。为了提高应用程序的性能,您必须先对其进行测量,然后才能对其进行改进。幸运的是,在过去几年中,我们的浏览器有了很多改进。一个这样的改进是用户计时 API。它并不是所有浏览器的正式组成部分,因为它只是 W3C 的建议,但除了 Safari 之外,所有主流浏览器的现代版本都支持它。我们不会在应用程序中部署我们的度量代码,因此缺少 Safari 支持虽然令人遗憾,但并不是交易杀手。
我知道有些人想知道为什么我们需要一种新的方法来测量时间。自 2009 年推出 ECMAScript 5.1 以来,我们已经有了Date.now()
和new Date().getTime()
版本。问题在于解决问题;充其量,Date.now()
可以以 1 毫秒的精度进行测量,但这还不够好。计算机可以在 1 毫秒内执行大量 JavaScript 指令。
用户计时 API 易于使用。我们不打算解释它的所有功能。我们将只展示足以帮助我们编写性能度量代码的内容。我们需要了解的第一个功能是performance.now()
。与Date.now()
类似,返回当前系统时间,但有两个重要区别:第一,返回浮点值,而不是像Date.now()
那样的整数值。浮点值表示 1 微秒或千分之一毫秒的精度。第二,performance.now()
是单调递增的,这是一种说它总是递增的奇特方式。这意味着,无论何时连续调用,第二次调用的值始终大于第一次调用的值。这是Date.now()
无法保证的。这可能看起来很奇怪,但Date.now()
并不是单调递增的。Date.now()
基于时间,大多数系统都有一个过程,通过每 15 或 20 分钟调整Date.now()
几毫秒来保持时间同步。因为Date.now()
的分辨率最多是一毫秒,任何在短时间内发生的事情都会被四舍五入到 0。一个简单的例子将有助于更好地解释问题:
function dateVsPerformance() {
var dNow1 = Date.now();
var dNow2 = Date.now();
var pNow1 = performance.now();
var pNow2 = performance.now();
console.info('date.now elapsed: ' + (dNow2 - dNow1));
console.info('performance.now elapsed: ' + (pNow2 - pNow1));
}
前面的代码非常简单。我们连续两次调用Date.now()
和performance.now()
,然后显示经过的时间。在大多数浏览器中,Date.now()
的运行时间为零,我们本能地知道这不可能是真的。不管你的电脑有多快;执行每一条指令总是需要一些时间。问题在于分辨率:Date.now()
以毫秒分辨率运行,JavaScript 指令需要几微秒才能执行。
1 毫秒等于 1000 微秒。
幸运的是,performance.now()
有微秒的分辨率;它总是显示任何两个调用之间的差异。连续调用时,它通常处于亚毫秒级。
Performance.now()
是一种非常有用的方法,但它不是性能工具箱中的唯一工具。用户计时 API 的创建者意识到,我们中的大多数人都将测量应用程序的性能,因此他们编写了一些方法,使其更容易实现。首先是performance.mark()
法;当传递字符串时,它将使用传递的字符串作为键在内部存储performance.now()
的值:
performance.mark('startTask1');
前面的代码存储了一个名为startTask1
的性能标记。
接下来是performance.measure()
。它将创建一个命名的计时度量。它有三个字符串作为参数。第一个字符串是度量的名称。第二个字符串是开始性能标记的名称,最后一个字符串是结束性能标记的名称:
performance.measure('task', 'startTask1', 'endTask1');
用户计时 API 将使用名称作为键在内部存储测量值。为了查看我们的绩效指标,我们只需要询问它们。最简单的方法是请求所有这些文件,然后循环它们以显示每个文件。以下代码演示了该技术:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<title>Chapter09 - User Timing API</title>
</head>
<body>
<script>
function showMeasurements() {
console.log('\n');
var entries = performance.getEntriesByType('measure');
for (var i = 0; i < entries.length; i++) {
console.log('Name: ' + entries[i].name +' Start Time: ' + entries[i].startTime +' Duration: ' + entries[i].duration + '\n');
}
}
function delay(delayCount) {
for (var ndx = 0; ndx < delayCount; ndx++) {
}
}
function init() {
performance.mark('mark1');
delay(1000);
performance.mark('mark2');
delay(10);
performance.mark('mark3');
performance.measure('task1', 'mark1', 'mark2');
performance.measure('task2', 'mark2', 'mark3');
showMeasurements();
performance.clearMeasures();
}
$(document).ready(init);
</script>
</body>
</html>
前面的代码将其所有结果显示到浏览器控制台;文档中不显示任何内容。
该操作以挂接 document ready 事件的代码开始,该事件调用init()
函数。调用performance.mark()
为mark1
创建一个标记。然后,我们使用计数器值 1000 调用delay()
来模拟有用任务的性能,然后再调用performance.mark()
,这将创建另一个性能标记mark2
。我们再次调用delay()
,这次使用较小的计数器 10,并创建另一个性能标记mark3
。
我们现在有三个性能标记。为了确定每个模拟任务需要多长时间,我们需要使用performance.measure()
方法测量标记。它包含三个参数:测量的名称、初始标记的名称和最终标记的名称。每次测量都将记录并存储在性能对象内部。为了查看测量结果,我们称之为showMeasurements()
方法。
showMeasurements()
方法以调用performance.getEntriesByType('measure')
开始。此方法返回一个数组,其中包含性能对象记录的所有性能度量。数组中的每个项都是一个对象,其中包含性能度量的名称、开始时间和持续时间。它还包含其性能类型,但我们不显示它。
我们做的最后一件事是打电话给performance.clearMeasures()
。请记住,performance 对象在内部存储所有标记和度量。如果你不偶尔清除它们,你的度量清单可能会变得很长。当在没有参数的情况下调用performance.clearMeasures()
时,它会清除它保存的所有度量值。也可以使用要清除的度量值的名称来调用它。您可以通过调用performance.clearMarks()
轻松清除已保存的标记。不使用参数调用它将清除所有保存的标记,使用标记名称调用它将清除标记。
现在我们有了一种测量 JavaScript 性能的方法,让我们来测量一些 jQuery:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<title>Chapter 9 - Measure jQuery</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Measuring jQuery</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<div class="form-group">
<input type="text" placeholder="Email" class="form-control">
</div>
<div class="form-group">
<input type="password" placeholder="Password" class="form-control">
</div>
<button type="submit" class="btn btn-success">Sign in</button>
</form>
</div>
</div>
</nav>
在前面的代码中没有什么棘手的事情,真的。它使用引导和 jQuery 创建导航栏。nav
酒吧功能不全;它只是让 jQuery 代码遵循一些标记进行解析:
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1>Chapter 9</h1>
<p>This is a template for a simple marketing or informational website. It includes a large callout called a
jumbotron and three supporting pieces of content. Use it as a starting point to create something more
unique.</p>
<p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more "</a></p>
</div>
</div>
<div class="container">
<!-- Example row of columns -->
<div class="row">
<div class="col-md-4 bosco">
<h2>First</h2>
<p>I am the first div. Initially I am the on the left side of the page. </p>
<p><a class="btn btn-default" href="#" role="button" name="alpha">View details "</a></p>
</div>
<div id="testClass" class="col-md-4 ">
<h2>Second</h2>
<p>I am the second div. I begin in-between the other two divs. </p>
<p><a id='find-me' class="btn btn-default find-me" href="#" role="button" name="beta">View details "</a></p>
</div>
<div class="col-md-4">
<h2>Third</h2>
<p>I am the third div. Initially I am on the right side of the page</p>
<p><a class="btn btn-default" href="http://www.google.com" role="button" name="delta">View details "</a></p>
</div>
</div>
<hr>
<form class="myForm">
<div class="input-group">
<select id="make" class="form-control">
<option value="buick">Buick</option>
<option value="cadillac">Cadillac</option>
<option value="chevrolet">Chevrolet</option>
<option value="chrysler">Chrysler</option>
<option value="dodge">Dodge</option>
<option value="ford">Ford</option>
</select>
</div>
<div class="input-group">
<select id="vehicleOptions" multiple class="form-control">
<option selected value="airConditioning">air conditioning</option>
<option value="cdPlayer">CD player</option>
<option selected value="satelliteRadio">satellite radio</option>
<option value="powerSeats">power seats</option>
<option value="navigation">navigation</option>
<option value="moonRoof">moon roof</option>
</select>
</div>
<div class="input-group">
<label for="comments" class="">Comments:</label>
<textarea id="comments" class="form-control"></textarea>
</div>
<div class="input-group">
<input type="text" id="firstName" class="form-control" placeholder="first name" value="Bob"/>
<input type="text" id="lastName" class="form-control" value="" placeholder="last name"/>
</div>
</form>
<hr>
<footer>
<p>© Company 2015</p>
</footer>
</div>
前面的标记是主要内容。再一次,我们只是给自己一些丰富的 HTML 来解析:
<script type="text/javascript">
function showMeasurements() {
console.log('\n');
var entries = performance.getEntriesByType('measure');
for (var i = 0; i < entries.length; i++) {
console.log('Name: ' + entries[i].name +
' Duration: ' + entries[i].duration + '\n');
}
}
function init() {
var ptr1, ptr2;
performance.mark('mark1');
ptr1 = $('#testClass > .find-me');
performance.mark('mark2');
ptr2 = $('#testClass').find('#find-me');
performance.mark('mark3');
performance.measure('with selectors', 'mark1', 'mark2');
performance.measure('selector+find ', 'mark2', 'mark3');
showMeasurements();
performance.clearMeasures();
}
$(document).ready(init);
</script>
</body>
</html>
前面的代码测量两个不同 jQuery 代码段的速度。两个代码段都返回一个指向同一元素的 jQuery 对象:唯一的锚定标记,其类为find-me
。有更快的方法可以找到元素,我们将在稍后讨论这些方法,但现在,我们希望解决测量技术的一个问题。
当代码运行时,它在控制台中显示两个测量值。第一个度量是使用选择器查找 jQuery 对象所用的时间。第二个测量是关于使用id
选择器与find()
方法相结合。第二种方法更优化,应该更快。
当您反复运行测试代码时,问题最为明显。每次运行的时间会有所不同,但它们可能会变化太大,以至于有时,应该更快的代码会更慢。再次运行计时代码,它会突然变得更快。发生什么事?虽然 JavaScript 是单线程的,我们不能中断代码,但浏览器不是单线程的,操作系统也不是。有时,当我们的测试代码运行时,另一个线程上可能会发生其他事情,导致它看起来比较慢。我们能做些什么来解决这个问题?
答案是使用平均法则,执行我们的代码足够多的时间来平衡偶尔的打嗝。考虑到这一点,下面是我们计时代码的改进版本。标记与上一版本相同;只有<script>
标签内的代码已更改:
<script type="text/javascript">
function showMeasurements() {
console.log('\n');
var entries = performance.getEntriesByType('measure');
for (var i = 0; i < entries.length; i++) {
console.log('Name: ' + entries[i].name +
' Duration: ' + entries[i].duration + '\n');
}
}
function multiExecuteFunction(func) {
var ndx, counter = 50000;
for (ndx = 0; ndx < counter; ndx += 1) {
func();
}
}
function init() {
performance.mark('mark1');
multiExecuteFunction(function () {
var ptr1 = $('#testClass > .find-me');
});
performance.mark('mark2');
multiExecuteFunction(function () {
var ptr2 = $('#testClass').find('#find-me');
});
performance.mark('mark3');
performance.measure('with selectors', 'mark1', 'mark2');
performance.measure('selector+find ', 'mark2', 'mark3');
showMeasurements();
performance.clearMeasures();
}
$(document).ready(init);
</script>
在新的版本的代码中,我们唯一更改的是如何调用 jQuery 代码。我们将它传递给一个调用它数千次的函数,而不是只调用一次。应该调用代码的实际次数由您决定。我喜欢叫它 10000 到 100000 次。
现在我们有了一个非常简单和精确的方法来测量代码的速度。请记住,我们不应该在生产网站上部署性能度量代码。让我们深入了解 jQuery 选择器,以便了解如何使用正确的选择器来显著提高代码的性能。
关于选择器,首先要了解的是,它们是对浏览器文档对象模型(DOM)的调用,并且与 DOM 的所有交互都很慢。即使知道 DOM 很慢的开发人员有时也不理解 jQuery 像所有代码一样使用 DOM,从而将标记呈现到浏览器页面。选择器是 jQuery 的核心,选择器之间的细微差异会使代码的速度产生巨大差异。对我们来说,理解如何编写快速高效的选择器是很重要的。
最快的选择器是基于最快的底层 DOM 代码的选择器。基于 DOM 的快速元素查找方法是document.getElementById()
,因此最快的 jQuery 选择器是基于id
选择器的。
这并不意味着您应该在标记中的每个元素上都放置 ID。如果有必要,您应该继续在元素上使用 ID,并使用id
选择器快速查找元素或元素附近的元素。
每次调用 jQuery 来评估选择器都是对处理时间的重大投资。jQuery 必须首先解析选择器,调用 DOM 方法来执行选择器,最后将结果转换为 jQuery 对象。请记住,DOM 是缓慢的。幸运的是,您可以缓存选择器。您的代码在第一次调用时仍然会受到时间惩罚,但后续调用会尽可能快。
只要不执行繁重的 DOM 操作,此方法就可以工作。我所说的重,是指从页面中添加或删除元素或其他使缓存选择器无效的内容。
并非所有选择器都创建为相等。选择正确的选择器可以大大提高应用程序的性能,但选择正确的选择器可能很棘手。以下是一些帮助您创建正确选择器的提示。请记住,当对性能有疑问时,请对其进行测量。
jQuery 的核心深处是 Sizzle 选择器引擎。Sizzle 从右向左读取。因此,最具体的选择器应该位于右侧。假设我们试图找到一个类为bubble
的<p>
标记。我们如何优化选择器?让我们看一个例子:
var unoptimized = $('div.col-md-4 .bubble');
我们的第一次尝试看起来不错。但是我们知道最右边的位置应该有最具体的选择器,所以我们在第二个示例中做了一些修改:
var optimized = $('.col-md-4 p.bubble');
在大多数浏览器中,这将比第一个示例稍微快一点。在以前版本的 jQuery 中,差异更大。不过,别担心;我们还有更多的优化。
作为开发人员,我们有时会做得过火。这在定义选择器时尤其糟糕。如果您添加了比所需的选择器更多的选择器来查找您要查找的元素,那么 jQuery 所做的工作将比所需的多。尽量将选择器减少到所需的数量。
让我们看一个例子:
// Too specific – don't do this
var overlySpecific = $('div.container div.row div.col-md-4 p.bubble');
此选择器增加了比所需更多的特异性,使其比前面的示例慢。您的选择器应该足够具体,以找到所需的元素,而不是更多。
默认情况下,jQuery 将搜索整个文档,查找与您的查询匹配的内容。通过缩小搜索范围来帮助解决这个问题。
如果我们想更快,而又不让过多的 ID 污染我们的标记,该怎么办?我们利用具有以下 ID 的最近父标记:
var fastOptimized = $('#testClass').find('.bubble');
即将到来的优化可以更好地称为经验法则。它们会让你的代码更快,但不会太快。幸运的是,它们很容易理解。
为了加速 jQuery 代码,更新到最新版本可能是最简单的事情之一。在升级到新版本的 jQuery 时,您应该格外小心,但是除了新功能外,升级通常还带来了更快的速度。既然您知道了如何度量代码的性能,那么就可以在更改版本之前和之后对其进行度量,以查看情况是否有所改善。
不要期望性能有巨大的变化,阅读发行说明,看看是否有任何突破性的变化。
目前,jQuery 有两个分支:1.x 分支和 2.x 分支。如果需要支持旧版本的 Internet Explorer,则只应使用 1.x 分支。如果您的网站只在现代浏览器上运行,并且 InternetExplorer9 是您需要支持的最旧版本,那么您应该切换到 jQuery 的 2.x 分支。
jQuery 的 2.x 分支消除了对 InternetExplorer6、7 和 8 的支持以及随之而来的所有头痛问题。这使得代码的执行速度加快了一点,同时也使库变小了,因此下载速度更快。
不推荐使用的方法是 jQuery 开发团队决定在未来版本中删除的那些方法。实际删除该方法可能需要几年时间。您应该尽快从代码中删除这些方法。该方法被弃用的原因可能不是性能,但可以肯定的是,jQuery 团队不会浪费时间优化标记为弃用的方法。
最快的代码是不运行的代码。事件处理后的默认行为是将其传递给父元素,然后一次又一次地传递给其父元素,直到到达根文档。所有这些冒泡都需要时间,如果您已经完成了所有必需的处理,那么可能会浪费时间。
幸运的是,通过在事件处理程序中调用event.preventDefault()
可以很容易地防止这种默认行为。这将阻止不必要的代码执行,并提高应用程序的速度。
始终记住访问 DOM 是一个缓慢的过程。在循环中访问它,您将使问题复杂化。最好将 DOM 的部分复制到 JavaScript 中,修改它,然后再复制回来。在本例中,我们将修改一个 DOM 元素,然后将其与修改不在 DOM 中的元素的几乎相同的代码进行比较:
function showMeasurements() {
console.log('\n');
var entries = performance.getEntriesByType('measure');
for (var i = 0; i < entries.length; i++) {
console.log('Name: ' + entries[i].name +
' Duration: ' + entries[i].duration + '\n');
}
}
function multiExecuteFunction(func) {
var ndx, counter = 50000;
for (ndx = 0; ndx < counter; ndx += 1) {
func();
}
}
function measurePerformance() {
console.log('\n');
var entries = performance.getEntriesByType('measure');
for (var i = 0; i < entries.length; i++) {
console.log('Name: ' + entries[i].name +
' Duration: ' + entries[i].duration + '\n');
}
}
function init() {
// unoptimized, modifying the DOM in a loop
var cnt = 0;
performance.mark('mark1');
var $firstName = $('.myForm').find('#firstName');
multiExecuteFunction(function () {
$firstName.val('Bob ' + cnt);
cnt += 1;
});
performance.mark('mark2');
// Second optimized, modifying a detached object
var myForm = $('.myForm');
var parent = myForm.parent();
myForm.detach();
cnt = 0;
var $firstName = $('.myForm').find('#firstName');
multiExecuteFunction(function () {
$firstName.val('Bob ' + cnt);
cnt += 1;
});
parent.append(myForm);
performance.mark('mark3');
performance.measure('DOM mod in loop ', 'mark1', 'mark2');
performance.measure('Detached in loop', 'mark2', 'mark3');
measurePerformance();
}
$(document).ready(init);
在前面的代码中,所有操作都在init()
方法中。我们正在修改<input>
标记的值。在第一个未优化的过程中,我们修改了循环中的 DOM。我们做了一些聪明的事情,比如在循环开始之前将选择器缓存到变量中。这段代码一开始似乎相当快。
在第二步中,在开始操作元素之前,我们将元素从 DOM 中分离出来。为了做到这一点,我们实际上必须编写更多的代码。首先,我们将表单缓存到名为myForm
的变量中。然后,我们也将其父对象缓存到一个变量中。接下来,我们使用 jQuery 的detach()
方法从 DOM 中分离myForm
。
循环中的代码与第一个版本中的代码相同。一旦退出循环,我们将myForm
附加到其父级,以恢复 DOM。虽然第二个版本中有更多的 JavaScript 代码,但速度大约是第一个版本的 5 倍。这是一种值得追求的性能提升。
jQuery 是有史以来最流行的 JavaScript 开源库。在排名前 10 万的网站中,60%以上的网站都使用了它。但这并不意味着您应该始终使用 jQuery;普通 JavaScript 有时是更好的选择。
当您希望查找具有 ID 的 DOM 元素时,调用document.getElementById()
DOM 方法比使用 jQuery 更快。为什么?因为这正是 jQuery 在解释选择器之后要做的事情。如果您不需要 jQuery 对象,只需要元素,请节省几微秒宝贵的时间,让 DOM 自己调用。打电话很容易:
var idName = document.getElementById('idName');
该方法接受一个参数:id
元素的名称。请注意,它的名称前面没有 hashtag。这不是 jQuery。如果找到元素,则返回对元素对象的引用。如果未找到,则返回null
。同样,请记住,返回的值不是 jQuery 对象,因此它不会附加 jQuery 方法。
还有其他可用的本机浏览器方法,一般来说,它们比用 JavaScript 编写的代码更快,无论是 jQuery、您自己的代码还是其他库中的代码。其他两种方法是document.getElementsByTag()
和document.getElementsByClassName()
。它们返回一个HTMLCollection
对象,它是任何类似数组的元素集合。如果未找到匹配项,则集合为空且长度为零。
较旧的浏览器,如 Internet Explorer 8,没有document.getElementsByClassName()
。因此,如果您需要支持较旧的浏览器,则应在使用之前检查此方法是否存在。jQuery 足够聪明,可以使用本机浏览器的版本(如果有),或者使用自己的代码(如果缺少)。
jQuery 和 JavaScript 对很多事情都很有用,但它们不应该用于所有事情。使用 CSS 通常可以更流畅、更快地完成 DOM 元素的动画制作、旋转、变换和转换。jQuery 有一些使用 CSS 的方法。然而,通过编写自己的 CSS,您可以获得根据需要定制的结果。CSS 可以利用主机系统的图形处理器单元(GPU)生成 jQuery/JavaScript 无法复制的结果。
我们从学习如何度量代码的性能开始本章。然后,当我们学习如何编写更好更快的选择器时,我们通过测量不同选择器的速度来使用这些知识。我们还学习了一些提高代码速度的 jQuery 最佳实践。我们在结束本章时意识到 jQuery 并不总是答案。有时,更好的代码来自于使用普通的老 JavaScript 或 DOM 方法。
在最后一章中,我们将介绍 jQuery 插件。插件是一些令人惊叹的功能,它们都封装在易于使用的软件包中。功能使我们能够轻松地向应用程序添加图形小部件,如日历、滑块和照片传送带。我们将学习如何使用插件,在哪里找到它们,最后,如何编写我们自己的插件。