首页 > 其他分享 >jQuery3-学习手册-全-

jQuery3-学习手册-全-

时间:2024-05-19 20:21:22浏览次数:26  
标签:jQuery jQuery3 元素 使用 手册 学习 选择器 方法 我们

jQuery3 学习手册(全)

原文:zh.annas-archive.org/md5/B3EDC852976B517A1E8ECB0D0B64863C

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我从 2007 年开始使用 jQuery,到现在仍在使用。当然,自那时以来发生了很多事情:新的 JavaScript 库,浏览器之间更一致的性能,以及对 JavaScript 本身的增强。10 年来唯一没有改变的事情是 jQuery 的表现力和简洁性。即使今天有很多新潮的东西,jQuery 仍然是快速高效完成工作的首选工具。

这本书有着悠久的历史,已经在第五版中保持完整。这本书之所以成功,是因为它直截了当,易于理解。我尽力保留了这本书效果良好的部分。我的目标是将学习 jQuery 现代化,适应当前的 Web 开发环境。

本书涵盖内容

第一章,入门,让你初步了解 jQuery JavaScript 库。本章首先描述了 jQuery 及其对你的作用。然后,它将指导你下载和设置库,以及编写你的第一个脚本。

第二章,选择元素,教你如何使用 jQuery 的选择器表达式和 DOM 遍历方法在页面上找到元素,无论它们在哪里。你将使用 jQuery 对各种页面元素应用样式,有时以纯 CSS 无法实现的方式。

第三章,处理事件,引导你了解 jQuery 的事件处理机制,以在浏览器事件发生时触发行为。你将看到 jQuery 如何轻松地不显眼地将事件附加到元素,甚至在页面加载完成之前。此外,你将获得更深入的主题概述,如事件冒泡、委托和命名空间。

第四章,样式与动画,向你介绍了 jQuery 的动画技术以及如何使用令人愉悦且有用的效果隐藏、显示和移动页面元素。

第五章,操作 DOM,教你如何随时更改你的页面。本章还将教你如何改变 HTML 文档的结构,并在其内容上添加内容。

第六章,使用 Ajax 发送数据,指导你通过许多方式使用 jQuery 轻松访问服务器端功能,而不必借助笨拙的页面刷新。掌握了库的基本组件,你将准备好探索库如何扩展以适应你的需求。

第七章,使用插件,向你展示如何查找、安装和使用插件,包括强大的 jQuery UI 和 jQuery Mobile 插件库。

第八章,开发插件,教你如何充分利用 jQuery 令人印象深刻的扩展能力从零开始开发自己的插件。您将创建自己的实用函数,添加 jQuery 对象方法,并探索 jQuery UI 组件工厂。接下来,您将再次浏览 jQuery 的基本组成部分,学习更高级的技术。

第九章,高级选择器和遍历,完善您对选择器和遍历的知识,获得优化选择器以提高性能、操纵 DOM 元素堆栈以及编写扩展选择和遍历功能的插件的能力。

第十章,高级事件,深入探讨诸如委托和节流等技术,这些技术可以大大提高事件处理的性能。您还将创建自定义和特殊事件,为 jQuery 库添加更多功能。

第十一章,高级效果,向您展示如何微调 jQuery 的视觉效果,通过制作自定义缓动函数和对每个动画步骤做出反应来实现。您将获得操作动画的能力,以及使用自定义队列安排操作的能力。

第十二章,高级 DOM 操作,为您提供了更多练习,通过诸如向元素附加任意数据的技术来修改 DOM。您还将学习如何扩展 jQuery 处理元素 CSS 属性的方式。

第十三章,高级 Ajax,帮助您更好地了解 Ajax 交易,包括用于处理稍后可能可用的数据的 jQuery deferred 对象系统。

附录 A,使用 QUnit 进行 JavaScript 测试,教你关于 QUnit 库,该库用于对 JavaScript 程序进行单元测试。这个库将成为您开发和维护高度复杂的网络应用程序工具包的重要补充。

附录 B,快速参考,提供了整个 jQuery 库的概览,包括其每一个方法和选择器表达式。其易于扫描的格式非常适合在您知道要做什么,但不确定正确的方法名称或选择器时使用。

本书所需内容

为了运行本书示例中演示的示例代码,您需要一款现代的网络浏览器,如 Google Chrome、Mozilla Firefox、Apple Safari 或 Microsoft Edge。

要尝试示例和完成章节结尾的练习,您还需要以下内容:

  • 基本文本编辑器

  • 浏览器的 Web 开发工具,例如 Chrome 开发者工具或 Firebug(如 第一章 入门 中所述的 使用开发工具 部分)

  • 每一章的完整代码包,其中包括 jQuery 库的副本(见 下载示例代码 部分)

此外,要运行 第六章 使用 Ajax 发送数据 及其后面的一些 Ajax 示例,您将需要安装 Node.js。

这本书适合谁

这本书非常适合客户端 JavaScript 开发人员。您不需要有任何 jQuery 的先前经验,尽管需要基本的 JavaScript 编程知识。

约定

在本书中,您会发现一些用于区分不同类型信息的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄以如下方式显示:"当我们指示 jQuery 查找所有具有 collapsible 类的元素并隐藏它们时,无需遍历每个返回的元素。"

代码块设置如下:

body { 
  background-color: #fff; 
  color: #000; 
  font-family: Helvetica, Arial, sans-serif; 
}
h1, h2, h3 { 
  margin-bottom: .2em; 
}
.poem { 
  margin: 0 2em; 
} 
.highlight { 
  background-color: #ccc; 
  border: 1px solid #888; 
  font-style: italic; 
  margin: 0.5em 0; 
  padding: 0.5em; 
} 

新术语重要单词 以粗体显示。您在屏幕上看到的词,例如菜单或对话框中的词,会以这种方式出现在文本中:"Sources 标签允许我们查看页面上加载的所有脚本的内容。"

警告或重要提示会以此类框的形式出现。

提示和技巧以这种方式出现。

第一章:入门

今天的万维网WWW)是一个动态的环境,其用户对站点的样式和功能设置了很高的标准。为了构建有趣和交互式的站点,开发人员正在转向 JavaScript 库,如 jQuery,来自动执行常见任务并简化复杂任务。jQuery 库之所以成为热门选择的原因之一是其能够帮助完成各种任务。

由于 jQuery 执行了许多不同的功能,因此很难知道从哪里开始。然而,该库的设计具有一致性和对称性;许多概念都借鉴自HTML层叠样式表CSS)的结构。这种设计适合对编程经验较少的设计师快速入门,因为许多人对这些技术的了解比对 JavaScript 更多。事实上,在本章中,我们将只用三行代码编写一个功能齐全的 jQuery 程序。另一方面,有经验的程序员也会欣赏到这种概念上的一致性。

在本章中,我们将涵盖:

  • jQuery 的主要特性

  • 设置 jQuery 代码环境

  • 一个简单的工作中的 jQuery 脚本示例

  • 选择 jQuery 而不是普通 JavaScript 的原因

  • 常见的 JavaScript 开发工具

jQuery 做了什么?

jQuery 库为常见的网页脚本提供了一个通用的抽象层,因此在几乎每种脚本情况下都很有用。它的可扩展性意味着我们永远无法在一本书中涵盖所有可能的用途和功能,因为插件不断被开发用来添加新的功能。然而,核心特性却能帮助我们完成以下任务:

  • 访问文档中的元素:没有 JavaScript 库,Web 开发人员通常需要编写许多代码行来遍历文档对象模型DOM)树并定位 HTML 文档结构的特定部分。有了 jQuery,开发人员可以使用强大而高效的选择器机制,轻松地检索需要检查或操作的文档的确切部分。
$('div.content').find('p'); 

  • 修改网页的外观:CSS 提供了一种影响文档呈现方式的强大方法,但当不是所有的 web 浏览器都支持相同的标准时,它会显得不足。使用 jQuery,开发人员可以弥补这一差距,依赖于所有浏览器的相同标准支持。此外,jQuery 可以在页面呈现后改变应用于文档部分的类别或个别样式属性。
$('ul > li:first').addClass('active'); 

  • 修改文档的内容:jQuery 不仅仅局限于表面上的改变,它可以用几个按键来修改文档本身的内容。文本可以被更改,图像可以被插入或交换,列表可以被重新排序,甚至可以用单个易于使用的应用程序编程接口API)重写和扩展 HTML 的整个结构。
$('#container').append('<a href="more.html">more</a>'); 

  • 响应用户的交互: 即使是最复杂和强大的行为,如果我们无法控制它们发生的时间,也是没有用的。jQuery 库提供了一种优雅的方式来拦截各种事件,比如用户点击链接,而不需要用事件处理程序来混杂 HTML 代码本身。
$('button.show-details').click(() => { 
  $('div.details').show(); 
});

  • 动画文档中正在进行的更改: 要有效地实现这样的交互行为,设计者还必须为用户提供视觉反馈。jQuery 库通过提供一系列效果,如淡入淡出和擦除,以及用于制作新效果的工具包,来促进这一点。
$('div.details').slideDown(); 

  • 在不刷新页面的情况下从服务器检索信息: 这种模式被称为 Ajax,最初代表 异步 JavaScript 和 XML,但后来已经成为了一套更大的用于客户端和服务器之间通信的技术集合。jQuery 库从这个过程中移除了特定于浏览器的复杂性,使开发者可以专注于服务器端功能。
$('div.details').load('more.html #content');

为什么 jQuery 的效果好?

随着对动态 HTML 的兴趣重新涌现,JavaScript 框架也在不断涌现。有些是专门的,只关注先前提到的一两个任务。其他尝试列出每一个可能的行为和动画,并提供预打包的。为了保持先前列出的广泛功能范围,同时保持相对紧凑,jQuery 采用了几种策略:

  • 利用 CSS 知识: 通过基于 CSS 选择器定位页面元素的机制,jQuery 继承了一种简洁而易读的表达文档结构的方式。由于专业网页开发的先决条件是对 CSS 语法的了解,因此 jQuery 库成为了设计师想要为其页面添加行为的入口点。

  • 支持扩展: 为了避免“功能蔓延”,jQuery 将特殊用例委托给插件。创建新插件的方法简单而且有文档说明,这推动了各种富有创意和有用的模块的开发。即使基本 jQuery 下载包中的大多数功能都是通过插件架构内部实现的,如果需要,也可以删除,从而获得更小的库。

  • 抽象出浏览器的怪癖: 网页开发的一个不幸现实是,每个浏览器都有自己的一套与发布标准不一致的特性。任何一个网页应用的一个重要部分都可能被归类为在每个平台上以不同方式处理功能。虽然不断发展的浏览器环境使得对于某些高级功能来说,无法实现完全与浏览器无关的代码库成为可能,但 jQuery 添加了一个抽象层,规范了常见任务,减少了代码量的同时极大地简化了它。

  • 始终与集合一起工作:当我们指示 jQuery 查找所有具有 collapsible 类的元素并隐藏它们时,没有必要遍历每个返回的元素。相反,像 .hide() 这样的方法被设计为自动在对象集合上工作,而不是在单个对象上工作。这种技术称为隐式迭代,意味着许多循环结构变得不再必要,大大减少了代码量。

  • 允许一行中进行多个操作:为了避免过多使用临时变量或者重复浪费,jQuery 使用一种被称为链式调用的编程模式来执行其大多数方法。这意味着对对象的大多数操作的结果都是对象本身,准备好接受下一个操作。

这些策略使 jQuery 包的文件大小保持较小,同时为我们的自定义代码提供了保持紧凑的技巧,以及使用该库。

这个库的优雅部分是由设计部分和由项目周围蓬勃发展的活跃社区所推动的进化过程造成的。jQuery 的用户聚集在一起讨论的不仅是插件的开发,还包括对核心库的增强。用户和开发人员还协助不断改进官方项目文档,这些文档可以在 api.jquery.com 找到。

尽管构建这样一个灵活而强大的系统需要付出巨大的努力,但最终产品却是供所有人免费使用的。这个开源项目在 MIT 许可证下授权,允许在任何网站上免费使用 jQuery,并促进其在专有软件中的使用。如果一个项目需要,开发者可以重新将 jQuery 授权为 GNU 公共许可证,以便包含在其他 GNU 许可的开源项目中。

jQuery 3 有什么新特性?

与 jQuery 2 引入的变化相比,jQuery 3 引入的变化相当微妙。大多数变化都在幕后进行。让我们简要地看一下一些变化以及它们对现有 jQuery 项目的影响。您可以在阅读本书的同时查看细粒度的详细信息(jquery.com/upgrade-guide/3.0)。

浏览器支持

jQuery 3 中浏览器支持的最大变化是 Internet Explorer。不得不支持此浏览器的旧版本是任何网页开发人员的噩梦。jQuery 3 通过仅支持 IE9+ 迈出了重要的一步。其他浏览器的支持政策是当前版本和上一个版本。

Internet Explorer 的时代已经屈指可数。微软发布了 IE 的继任者 Edge。这个浏览器是完全独立于 IE 的项目,不会受到一直困扰 IE 的问题的影响。此外,最近版本的 Microsoft Windows 实际上推动 Edge 成为默认浏览器,并且更新是定期且可预测的。再见了,IE,真是一去不复返。

延迟对象

Deferred 对象在 jQuery 1.5 中引入,作为更好地管理异步行为的手段。它们有点像 ES2015 的 promises,但不同之处足以使它们不能互换。现在,随着 ES2015 版本的 JavaScript 在现代浏览器中变得普遍,Deferred 对象与原生 Promise 对象完全兼容。这意味着旧的 Deferred 实现发生了相当大的变化。

异步文档准备

起初,文档准备好的回调函数被异步执行的想法可能看起来有些违反直觉。在 jQuery 3 中之所以会这样,有几个原因。首先,$(() => {}) 表达式返回一个 Deferred 实例,这些现在的行为类似于原生 Promise。第二个原因是存在一个 jQuery.ready promise,在文档准备好时解析。正如你在本书后面将看到的,你可以在 DOM 准备好渲染之前使用此 promise 以及其他 promise 来执行其他异步任务。

其他所有内容

在 jQuery 3 中引入了许多其他 API 的破坏性更改,我们在这里不会详细讨论。我之前提到的升级指南详细介绍了每个更改以及如何处理它们。然而,当我们在本书中逐步进行时,我会指出 jQuery 3 中的新功能或不同之处。

制作我们的第一个由 jQuery 驱动的网页

现在我们已经介绍了使用 jQuery 提供的一系列功能,我们可以看看如何将库投入实际运用。要开始,我们需要下载 jQuery 的副本。

下载 jQuery

无需安装。要使用 jQuery,我们只需要一个公开可用的文件副本,无论该副本是在外部站点还是我们自己的站点上。由于 JavaScript 是一种解释性语言,因此无需担心编译或构建阶段。每当我们需要一个页面具有可用的 jQuery,我们只需在 HTML 文档中的 <script> 元素中引用文件的位置即可。

官方 jQuery 网站 (jquery.com/) 总是具有最新的稳定版本的库,可以直接从网站的首页下载。任何时候可能有几个版本的 jQuery 可用;对于我们作为站点开发人员而言,最合适的版本将是库的最新未压缩版本。在生产环境中,可以用压缩版本替换此版本。

随着 jQuery 的普及,公司已经通过其 内容交付 网络CDN)免费提供文件。尤其是 Google (developers.google.com/speed/libraries/devguide)、Microsoft (www.asp.net/ajaxlibrary/cdn.ashx) 和 jQuery 项目本身 (code.jquery.com) 在全球范围内分布了强大、低延迟的服务器上提供该文件,以便用户快速下载,而不管用户位于何处。尽管由 CDN 托管的 jQuery 副本具有由于服务器分发和缓存而带来的速度优势,但在开发过程中使用本地副本可能更加方便。在本书中,我们将使用存储在我们自己系统上的文件副本,这样无论我们是否连接到互联网,都可以运行我们的代码。

为了避免意外错误,始终使用特定版本的 jQuery。例如,3.1.1。一些 CDN 允许您链接到库的最新版本。同样,如果您使用 npm 安装 jQuery,请始终确保您的 package.json 需要特定版本。

在 HTML 文档中设置 jQuery

大多数 jQuery 使用示例都由三部分组成:HTML 文档、用于样式的 CSS 文件,以及用于操作的 JavaScript 文件。对于我们的第一个示例,我们将使用一个包含书摘的页面,其中有许多类应用于其部分。此页面包括对 jQuery 库的最新版本的引用,我们已经下载并将其重命名为 jquery.js,并放置在我们的本地项目目录中:

<!DOCTYPE html> 

<html lang="en"> 
  <head> 
    <meta charset="utf-8"> 
    <title>Through the Looking-Glass</title> 

    <link rel="stylesheet" href="01.css"> 

    <script src="img/jquery.js"></script> 
    <script src="img/01.js"></script> 
  </head> 

  <body>   
    <h1>Through the Looking-Glass</h1> 
    <div class="author">by Lewis Carroll</div> 

    <div class="chapter" id="chapter-1"> 
      <h2 class="chapter-title">1\. Looking-Glass House</h2> 
      <p>There was a book lying near Alice on the table, 
        and while she sat watching the White King (for she 
        was still a little anxious about him, and had the 
        ink all ready to throw over him, in case he fainted 
        again), she turned over the leaves, to find some 
        part that she could read, <span class="spoken"> 
        "&mdash;for it's all in some language I don't know," 
        </span> she said to herself.</p> 
      <p>It was like this.</p> 
      <div class="poem"> 
        <h3 class="poem-title">YKCOWREBBAJ</h3> 
        <div class="poem-stanza"> 
          <div>sevot yhtils eht dna ,gillirb sawT'</div> 
          <div>;ebaw eht ni elbmig dna eryg diD</div> 
          <div>,sevogorob eht erew ysmim llA</div> 
          <div>.ebargtuo shtar emom eht dnA</div> 
        </div> 
      </div> 
      <p>She puzzled over this for some time, but at last 
        a bright thought struck her. <span class="spoken"> 
        "Why, it's a Looking-glass book, of course! And if 
        I hold it up to a glass, the words will all go the 
        right way again."</span></p> 
      <p>This was the poem that Alice read.</p> 
      <div class="poem"> 
        <h3 class="poem-title">JABBERWOCKY</h3> 
        <div class="poem-stanza"> 
          <div>'Twas brillig, and the slithy toves</div> 
          <div>Did gyre and gimble in the wabe;</div> 
          <div>All mimsy were the borogoves,</div> 
          <div>And the mome raths outgrabe.</div> 
        </div> 
      </div> 
    </div> 
  </body> 
</html> 

在普通的 HTML 前导部分之后,加载样式表。对于本示例,我们将使用一个简单的样式表:

body { 
  background-color: #fff; 
  color: #000; 
  font-family: Helvetica, Arial, sans-serif; 
}
h1, h2, h3 { 
  margin-bottom: .2em; 
}
.poem { 
  margin: 0 2em; 
} 
.highlight { 
  background-color: #ccc; 
  border: 1px solid #888; 
  font-style: italic; 
  margin: 0.5em 0; 
  padding: 0.5em; 
} 

获取示例代码

您可以从以下 GitHub 存储库访问示例代码:

github.com/PacktPublishing/Learning-jQuery-3

在引用样式表之后,包含 JavaScript 文件。重要的是,jQuery 库的 script 标签应放在我们自定义脚本的标签之前;否则,当我们的代码尝试引用它时,jQuery 框架将不可用。

在本书的其余部分,将仅打印 HTML 和 CSS 文件的相关部分。完整的文件可从该书的伴随代码示例中获取:github.com/PacktPublishing/Learning-jQuery-3

现在,我们的页面看起来像这样:

我们将使用 jQuery 为诗文文字应用新样式。

此示例旨在演示 jQuery 的简单用法。在现实世界的情况下,此类样式可以纯粹通过 CSS 执行。

添加我们的 jQuery 代码

我们的自定义代码将放在第二个目前为空的 JavaScript 文件中,我们通过<script src="img/01.js"></script>从 HTML 中包含。对于这个示例,我们只需要三行代码:

$(() => {
  $('div.poem-stanza').addClass('highlight')
});

在本书中,我将使用更新的 ES2015 箭头函数语法来编写大多数回调函数。唯一的原因是它比在各处使用function关键字更简洁。然而,如果你更喜欢function() {}语法,那么请尽管使用它。

现在让我们逐步分析这个脚本,看看它是如何工作的。

查找诗歌文本

jQuery 中的基本操作是选择文档的一部分。这通过$()函数完成。通常,它以字符串作为参数,该参数可以包含任何 CSS 选择器表达式。在本例中,我们希望找到文档中所有应用了poem-stanza类的<div>元素,因此选择器非常简单。但是,我们将在本书的过程中涵盖更多复杂的选项。我们将在第二章中介绍许多定位文档部分的方法,选择元素

当调用$()函数时,它会返回一个新的 jQuery 对象实例,这是我们将要使用的基本构建块。该对象封装了零个或多个 DOM 元素,并允许我们以多种不同的方式与它们交互。在这种情况下,我们希望修改页面的这些部分的外观,并通过更改应用于诗歌文本的类来实现这一目标。

注入新类

.addClass()方法,像大多数 jQuery 方法一样,其名称具有自解释性;它将一个 CSS 类应用于我们选择的页面部分。它的唯一参数是要添加的类的名称。这个方法及其相对应的.removeClass()方法将允许我们轻松地观察到 jQuery 在我们探索可用的不同选择器表达式时的作用。目前,我们的示例仅添加了highlight类,我们的样式表将其定义为具有灰色背景和边框的斜体文本。

注意,不需要迭代即可将类添加到所有诗歌的段落中。正如我们讨论的那样,jQuery 在诸如.addClass()之类的方法内部使用隐式迭代,因此只需一个函数调用即可更改文档中的所有选定部分。

执行代码

综合起来,$().addClass()足以实现我们改变诗歌文本外观的目标。但是,如果单独将这行代码插入文档头部,它将不会产生任何效果。JavaScript 代码一旦在浏览器中遇到就会运行,在处理标题时,尚未存在要样式化的 HTML。我们需要延迟执行代码,直到 DOM 可供我们使用。

使用 $(() => {}) 构造(传递函数而不是选择器表达式),jQuery 允许我们安排函数调用,以便一旦 DOM 加载完成,即可触发,而不必等待图像完全渲染。虽然这种事件调度在没有 jQuery 的情况下也是可能的,但 $(() => {}) 提供了一种特别优雅的跨浏览器解决方案,其中包括以下特性:

  • 当可用时,它使用浏览器的本机 DOM 就绪实现,并添加 window.onload 事件处理程序作为一个安全网

  • 即使在浏览器事件已经发生后调用,它也会执行传递给 $() 的函数

  • 它异步处理事件调度,以允许脚本延迟执行,如果有必要的话

$() 函数的参数可以接受对已定义函数的引用,如以下代码片段所示:

function addHighlightClass()  { 
  $('div.poem-stanza').addClass('highlight'); 
} 

$(addHighlightClass); 

列表 1.1

然而,如在脚本的原始版本中演示的,并在列表 1.2中重复的,该方法也可以接受匿名函数:

$(() =>
  $('div.poem-stanza').addClass('highlight')
); 

列表 1.2

这种匿名函数惯用法在 jQuery 代码中对于接受函数作为参数的方法很方便,当该函数不可重用时。此外,它创建的闭包可以是一种高级且强大的工具。如果您使用箭头函数,您还可以获得词法绑定的 this 作为上下文,这避免了绑定函数的需要。然而,如果不小心处理,它可能会产生意想不到的后果和内存使用方面的影响。

成品

现在我们的 JavaScript 就位了,页面看起来是这样的:

诗歌的节现在已经用盒子括起来,如 01.css 样式表所指定的,由于 JavaScript 代码插入了 highlight 类。

纯 JavaScript 对比 jQuery

即使是这样简单的任务,如果没有 jQuery 支持,也可能会变得复杂。在纯 JavaScript 中,我们可以这样添加 highlight 类:

window.onload = function() {
  const divs = document.getElementsByTagName('div');
  const hasClass = (elem, cls) =>
    new RegExp(` ${cls} `).test(` ${elem.className} `);

  for (let div of divs) {
    if (hasClass(div, 'poem-stanza') && !hasClass(div, 'highlight')) {
      div.className += ' highlight';
    }
  }
};

列表 1.3

尽管其长度较长,但这种解决方案并没有处理 jQuery 在列表 1.2中为我们处理的许多情况,例如:

  • 正确地尊重其他 window.onload 事件处理程序

  • 一旦 DOM 准备就绪就开始行动

  • 使用现代 DOM 方法优化元素检索和其他任务

我们可以看到,我们使用 jQuery 驱动的代码比其纯 JavaScript 等价物更容易编写、更容易阅读,并且执行速度更快。

使用开发工具

正如这个代码对比所显示的,jQuery 代码通常比其基本的 JavaScript 等价物更短更清晰。然而,这并不意味着我们将总是写出没有错误的代码,或者我们会在任何时候直观地理解页面上正在发生的事情。有了标准的开发工具,我们的 jQuery 编码体验将会更加顺畅。

所有现代浏览器都提供了高质量的开发工具。我们可以自由选择最适合我们的环境。选项包括以下内容:

每个工具包都提供类似的开发功能,包括:

  • 探索和修改 DOM 的各个方面

  • 调查 CSS 与其对页面呈现的影响之间的关系

  • 通过特殊方法方便地追踪脚本执行

  • 暂停正在运行的脚本的执行并检查变量值

尽管这些功能的细节因工具而异,但一般概念仍然相同。在本书中,一些示例将需要使用其中一个工具包;我们将使用 Chrome 开发者工具进行这些演示,但其他浏览器的开发工具也是很好的替代方案。

Chrome 开发者工具

最新的 Chrome 开发者工具的访问和使用说明可以在项目的文档页面上找到:developer.chrome.com/devtools。这些工具涉及太多,无法在此处详细探讨,但对一些最相关的功能进行概述将对我们有所帮助。

理解这些屏幕截图

Chrome 开发者工具是一个快速发展的项目,因此以下屏幕截图可能与您的环境不完全匹配。

当激活 Chrome 开发者工具时,会出现一个新的面板,提供有关当前页面的信息。在此面板的默认元素标签中,我们可以在左侧看到页面结构的表示,右侧可以看到所选元素的详细信息(例如适用于它的 CSS 规则)。此标签对于调查页面结构和调试 CSS 问题特别有用:

“源”标签允许我们查看页面上加载的所有脚本的内容。通过右键单击行号,我们可以设置断点,设置条件断点,或在达到另一个断点后使脚本继续到该行。断点是暂停脚本执行并逐步检查发生情况的有效方法。在页面右侧,我们可以输入要在任何时间知道其值的变量和表达式列表:

在学习 jQuery 时,控制台选项卡将是我们最频繁使用的。面板底部的字段允许我们输入任何 JavaScript 语句,然后语句的结果将显示在面板中。

在这个例子中,我们执行了与 列表 1.2 中相同的 jQuery 选择器,但是我们没有对所选元素执行任何操作。即便如此,该语句也给我们提供了有趣的信息:我们看到选择器的结果是一个指向页面上两个 .poem-stanza 元素的 jQuery 对象。我们可以随时使用此控制台功能快速尝试 jQuery 代码,直接从浏览器中进行:

另外,我们可以直接从我们的代码中使用 console.log() 方法与控制台进行交互:

$(() => {
  console.log('hello');
  console.log(52);
  console.log($('div.poem-stanza'));
});

列表 1.4

这段代码说明了我们可以将任何类型的表达式传递给 console.log() 方法。简单值如字符串和数字直接打印出来,而像 jQuery 对象这样的复杂值则以我们的检查方式进行了良好的格式化:

这个 console.log() 函数(在我们之前提到的每个浏览器开发工具中都有效)是 JavaScript alert() 函数的一个便利替代品,并且在我们测试 jQuery 代码时将非常有用。

摘要

在本章中,我们学习了如何使 jQuery 在我们的网页上的 JavaScript 代码中可用,使用 $() 函数来定位具有给定类的页面的某个部分,调用 .addClass() 来为页面的这部分应用附加样式,并调用 $(() => {}) 来使该函数在加载页面时执行。我们还探讨了在编写、测试和调试我们的 jQuery 代码时将依赖的开发工具。

现在我们知道为什么开发人员选择使用 JavaScript 框架而不是从头编写所有代码,即使是最基本的任务也是如此。我们还看到了 jQuery 作为框架的一些优点,以及为什么我们可能会选择它而不是其他选项,以及通常情况下,jQuery 使哪些任务更容易。

我们一直使用的简单示例展示了 jQuery 的工作原理,但在实际情况下并不太有用。在下一章中,我们将通过探索 jQuery 的复杂选择器语言来扩展这段代码,找到这种技术的实际用途。

第二章:选择元素

jQuery 库利用 层叠样式表 (CSS) 选择器的力量,让我们能够快速轻松地访问 文档对象模型 (DOM) 中的元素或元素组。

在本章中,我们将涵盖:

  • 网页上元素的结构

  • 如何使用 CSS 选择器在页面上查找元素

  • 当 CSS 选择器的特异性发生变化时会发生什么

  • 自定义 jQuery 扩展到标准的 CSS 选择器集

  • DOM 遍历方法,提供了更大的灵活性,用于访问页面上的元素

  • 使用现代 JavaScript 语言功能有效地迭代 jQuery 对象

理解 DOM

jQuery 最强大的方面之一是其使得在 DOM 中选择元素变得容易。DOM 作为 JavaScript 和网页之间的接口;它提供了 HTML 源代码的表示,作为对象网络,而不是作为纯文本。

这个网络采用了页面上元素的家族树形式。当我们提到元素彼此之间的关系时,我们使用与指家庭关系时相同的术语:父母、子女、兄弟姐妹等。一个简单的例子可以帮助我们理解家族树隐喻如何适用于文档:

<html> 
  <head> 
    <title>the title</title> 
  </head> 
  <body> 
    <div> 
      <p>This is a paragraph.</p> 
      <p>This is another paragraph.</p> 
      <p>This is yet another paragraph.</p> 
    </div> 
  </body> 
</html> 

在这里,<html> 是所有其他元素的祖先;换句话说,所有其他元素都是 <html> 的后代。<head><body> 元素不仅是 <html> 的后代,而且是其子元素。同样,除了是 <head><body> 的祖先之外,<html> 还是它们的父元素。<p> 元素是 <div> 的子元素(和后代),是 <body><html> 的后代,以及彼此的兄弟元素。

为了帮助可视化 DOM 的家族树结构,我们可以使用浏览器的开发者工具检查任何页面的 DOM 结构。当您好奇某个其他应用程序的工作方式,并且想要实现类似功能时,这特别有帮助。

有了这些元素的树结构,我们将能够使用 jQuery 高效地定位页面上的任何一组元素。我们实现这一目标的工具是 jQuery 选择器遍历方法

使用 $() 函数

由 jQuery 的选择器和方法生成的元素集合始终由 jQuery 对象表示。当我们想要实际对页面上找到的东西进行操作时,这些对象非常容易使用。我们可以轻松地将事件绑定到这些对象上,并向它们添加视觉效果,以及将多个修改或效果链接在一起。

请注意,jQuery 对象与普通 DOM 元素或节点列表不同,因此在某些任务上不一定提供相同的方法和属性。在本章的最后部分,我们将探讨直接访问 jQuery 对象中收集的 DOM 元素的方法。

要创建一个新的 jQuery 对象,我们使用 $() 函数。这个函数通常接受一个 CSS 选择器作为其唯一参数,并充当工厂,返回一个指向页面上相应元素的新 jQuery 对象。几乎任何可以在样式表中使用的东西也可以作为字符串传递给此函数,使我们能够将 jQuery 方法应用于匹配的元素集。

使 jQuery 与其他 JavaScript 库协同工作

在 jQuery 中,美元符号 ($) 只是 jQuery 的别名。因为 $() 函数在 JavaScript 库中非常常见,所以如果在同一页中使用了多个这些库,可能会出现冲突。我们可以通过在自定义 jQuery 代码中将每个 $ 实例替换为 jQuery 来避免这种冲突。有关此问题的其他解决方案将在第十章 高级事件中讨论。另一方面,jQuery 在前端开发中非常突出,因此库通常不会动 $ 符号。

选择器的三个主要构建块是标签名ID。它们可以单独使用,也可以与其他选择器组合使用。以下简单示例说明了这三个选择器在代码中的应用方式:

选择器类型 CSS jQuery 功能
标签名 p { } $('p') 这选择了文档中的所有段落。
ID #some-id { } $('#some-id') 这选择了文档中具有 ID 为 some-id 的单个元素。
.some-class { } $('.some-class') 这选择了文档中具有类 some-class 的所有元素。

如第一章 入门中所述,当我们调用 jQuery 对象的方法时,自动隐式地循环遍历了我们传递给 $() 的选择器所引用的元素。因此,我们通常可以避免显式迭代,比如 for 循环,在 DOM 脚本中经常需要。

现在我们已经介绍了基础知识,我们准备开始探索一些更强大的选择器使用方法。

CSS 选择器

jQuery 库支持 CSS 规范 1 到 3 中包含的几乎所有选择器,详细信息请参见万维网联盟的网站:www.w3.org/Style/CSS/specs。这种支持允许开发人员增强其网站,而无需担心哪些浏览器可能不理解更高级的选择器,只要浏览器启用了 JavaScript。

渐进增强

负责任的 jQuery 开发者应始终将渐进增强和优雅降级的概念应用于其代码,确保页面在禁用 JavaScript 时渲染的准确性与启用 JavaScript 时一样,即使不那么美观。我们将在本书中继续探讨这些概念。有关渐进增强的更多信息,请访问en.wikipedia.org/wiki/Progressive_enhancement。话虽如此,这些天即使在移动浏览器上也很少遇到禁用 JavaScript 的用户。

要开始学习 jQuery 如何与 CSS 选择器配合工作,我们将使用许多网站上经常出现的结构,通常用于导航——嵌套的无序列表:

<ul id="selected-plays"> 
  <li>Comedies 
    <ul> 
      <li><a href="/asyoulikeit/">As You Like It</a></li> 
      <li>All's Well That Ends Well</li> 
      <li>A Midsummer Night's Dream</li> 
      <li>Twelfth Night</li> 
    </ul> 
  </li> 
  <li>Tragedies 
    <ul> 
      <li><a href="hamlet.pdf">Hamlet</a></li> 
      <li>Macbeth</li> 
      <li>Romeo and Juliet</li> 
    </ul> 
  </li> 
  <li>Histories 
    <ul> 
      <li>Henry IV (<a href="mailto:[email protected]">email</a>) 
         <ul> 
           <li>Part I</li> 
           <li>Part II</li>  
         </ul> 
      <li><a href="http://www.shakespeare.co.uk/henryv.htm">Henry V</a></li>
      <li>Richard II</li> 
    </ul> 
  </li> 
</ul> 

可下载的代码示例

您可以从以下 Github 仓库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

注意,第一个<ul>具有selecting-plays的 ID,但没有任何<li>标签与之关联的类。没有应用任何样式,列表看起来像这样:

嵌套列表呈现我们所期望的样子——一组垂直排列的项目,根据它们的级别缩进。

设计列表项级别

假设我们只想要顶级项——喜剧、悲剧和历史——以及仅仅是顶级项水平排列。我们可以首先在样式表中定义一个horizontal类:

.horizontal { 
  float: left; 
  list-style: none; 
  margin: 10px; 
} 

horizontal类使元素浮动到其后面的左侧,如果是列表项,则删除其标志,并在其四周添加 10 像素的边距。

不直接在我们的 HTML 中添加horizontal类,而是仅将其动态添加到顶级列表项,以演示 jQuery 对选择器的使用:

$(() => {
  $('#selected-plays > li')
    .addClass('horizontal');
}); 

列表 2.1

如第一章所述,入门,我们通过调用$(() => {})开始 jQuery 代码,该代码在 DOM 加载后运行传递给它的函数,但在此之前不会运行。

第二行使用子级组合符(>)仅向所有顶级项添加horizontal类。实际上,$()函数内的选择器表示“找到每个列表项(li),它是具有 ID 为selected-plays#selected-plays)的元素的子级(>)”。

现在应用了该类,样式表中为该类定义的规则生效,这意味着列表项水平排列而不是垂直排列。现在,我们的嵌套列表看起来是这样的:

对所有其他项进行样式设置--即不在顶级的项--有很多种方法。由于我们已经将horizontal类应用于顶级项目,选择所有子级项目的一种方法是使用否定伪类来标识所有没有horizontal类的列表项:

$(() => {
  $('#selected-plays > li')
    .addClass('horizontal'); 
  $('#selected-plays li:not(.horizontal)')
    .addClass('sub-level');
}); 

列表 2.2

这一次我们选择了每个列表项(<li>),它:

  • 是具有 ID 为selected-plays的元素的后代(#selected-plays

  • 没有horizontal类(:not(.horizontal)

当我们向这些项目添加sub-level类时,它们将接收到样式表中定义的阴影背景:

.sub-level { 
  background: #ccc; 
} 

现在嵌套列表看起来是这样的:

选择器的具体性

在 jQuery 中,选择器的具体性有一个范围,从非常通用的选择器到非常具体的选择器。目标是选择正确的元素,否则你的选择器就会失效。jQuery 初学者的倾向是为所有东西实现非常具体的选择器。也许通过反复试验,他们已经通过为给定的选择器添加更多的具体性来修复选择器错误。然而,这并不总是最好的解决方案。

让我们看一个例子,增加顶级<li>文本的首字母大小。这是我们要应用的样式:

.big-letter::first-letter {
   font-size: 1.4em;
 }

下面是列表项文本的样式:

正如你所见,喜剧,悲剧和历史如预期地应用了big-letter样式。为了做到这一点,我们需要一个比仅仅选择$('#selected-plays li')更具体的选择器,后者会将样式应用于每一个<li>,甚至子元素。我们可以改变 jQuery 选择器的具体性以确保我们只获得我们所期望的:

$(() => { 
  $('#selected-plays > li') 
    .addClass('big-letter'); 

  $('#selected-plays li.horizontal')
    .addClass('big-letter'); 

  $('#selected-plays li:not(.sub-level)') 
    .addClass('big-letter'); 
});

列表 2.3

所有这三个选择器都做了同样的事情--将big-letter样式应用于#selected-plays中的顶级<li>元素。每个选择器的具体性都不同。让我们回顾一下每个选择器的工作原理以及它们的优势:

  • #selected-plays > li:这找到了直接是#selected-plays的子元素的<li>元素。这易于阅读,并且在 DOM 结构上语义相关。

  • #selected-plays li.horizontal:这找到了#selected-plays<li>元素或子元素,并具有horizontal类。这也很容易阅读,并强制执行特定的 DOM 模式(应用horizontal类)。

  • #selected-plays li:not(.sub-level):这很难阅读,效率低下,并且不反映实际的 DOM 结构。

在实际应用中,选择器的具体性经常会成为一个无穷的例子。每个应用都是独特的,正如我们刚才所看到的,实现选择器的具体性并没有一个正确的方法。重要的是,我们要通过考虑选择器对 DOM 结构的影响以及因此对应用或网站的可维护性的影响来行使良好的判断力。

属性选择器

属性选择器是 CSS 选择器的一个特别有用的子集。它们允许我们通过其 HTML 属性之一来指定一个元素,例如链接的title属性或图像的alt属性。例如,要选择所有具有alt属性的图像,我们写成这样:

$('img[alt]') 

设置链接的样式

属性选择器接受受到正则表达式启发的通配符语法,用于标识字符串开头(^)或结尾($)的值。它们还可以采用星号(*)来表示字符串中任意位置的值,感叹号(!)表示否定值。

假设我们希望为不同类型的链接使用不同的样式。我们首先在样式表中定义样式:

a { 
  color: #00c;  
} 
a.mailto { 
  background: url(images/email.png) no-repeat right top; 
  padding-right: 18px; 
} 
a.pdflink { 
  background: url(images/pdf.png) no-repeat right top; 
  padding-right: 18px; 
} 
a.henrylink { 
  background-color: #fff; 
  padding: 2px; 
  border: 1px solid #000; 
} 

然后,我们使用 jQuery 将三个类--mailtopdflinkhenrylink--添加到相应的链接中。

要为所有电子邮件链接添加一个类,我们构造一个选择器,查找所有具有href属性的锚元素(a),该属性以mailto:开头(^="mailto:"),如下所示:

$(() => {
  $('a[href^="mailto:"]')
    .addClass('mailto');
}); 

列表 2.4

由于页面样式表中定义的规则,邮件链接后会出现一个信封图像。

要为所有 PDF 文件的链接添加一个类,我们使用美元符号而不是插入符号。这是因为我们选择的是链接,其href属性以.pdf结尾:

$(() => { 
  $('a[href^="mailto:"]')
    .addClass('mailto'); 
  $('a[href$=".pdf"]')
    .addClass('pdflink'); 
}); 

列表 2.5

新添加的pdflink类的样式表规则会导致每个指向 PDF 文档的链接后面都出现 Adobe Acrobat 图标,如下面的截图所示:

属性选择器也可以组合使用。例如,我们可以将类henrylink添加到所有链接的href值既以http开头又在任何地方包含henry的链接中:

$(() => { 
  $('a[href^="mailto:"]')
    .addClass('mailto'); 
  $('a[href$=".pdf"]')
    .addClass('pdflink'); 
  $('a[href^="http"][href*="henry"]') 
    .addClass('henrylink'); 
}); 

列表 2.6

有了应用于三种类型链接的三个类,我们应该看到以下效果:

注意 Hamlet 链接右侧的 PDF 图标,电子邮件链接旁边的信封图标,以及 Henry V 链接周围的白色背景和黑色边框。

自定义选择器

jQuery 在广泛的 CSS 选择器基础上添加了自己的自定义选择器。这些自定义选择器增强了 CSS 选择器定位页面元素的能力。

性能说明

在可能的情况下,jQuery 使用浏览器的原生 DOM 选择器引擎来查找元素。当使用自定义 jQuery 选择器时,这种极快的方法是不可能的。因此,建议在原生选项可用时避免频繁使用自定义选择器。

大多数自定义选择器都允许我们从已经找到的一组元素中选择一个或多个元素。自定义选择器的语法与 CSS 伪类的语法相同,选择器以冒号(:)开头。例如,要从具有horizontal类的一组 <div> 元素中选择第二个项目,我们写成这样:

$('div.horizontal:eq(1)') 

请注意,:eq(1)选择集合中的第二个项目,因为 JavaScript 数组编号是以零为基础的,这意味着它从零开始。相比之下,CSS 是以 1 为基础的,因此像$('div:nth-child(1)')这样的 CSS 选择器将选择所有作为其父元素的第一个子元素的div选择器。由于很难记住哪些选择器是基于零的,哪些是基于一的,当存在疑惑时,我们应该在 jQuery API 文档api.jquery.com/category/selectors/中查阅 jQuery API 文档。

风格化交替行

在 jQuery 库中有两个非常有用的自定义选择器是:odd:even。让我们看看我们如何使用其中一个来对基本表格进行条纹处理,如下表格所示:

<h2>Shakespeare's Plays</h2> 
<table> 
  <tr> 
    <td>As You Like It</td> 
    <td>Comedy</td> 
    <td></td> 
  </tr> 
  <tr> 
    <td>All's Well that Ends Well</td> 
    <td>Comedy</td> 
    <td>1601</td> 
  </tr> 
  <tr> 
    <td>Hamlet</td> 
    <td>Tragedy</td> 
    <td>1604</td> 
  </tr> 
  <tr> 
    <td>Macbeth</td> 
    <td>Tragedy</td> 
    <td>1606</td> 
  </tr> 
  <tr> 
    <td>Romeo and Juliet</td> 
    <td>Tragedy</td> 
    <td>1595</td> 
  </tr> 
  <tr> 
    <td>Henry IV, Part I</td> 
    <td>History</td> 
    <td>1596</td> 
  </tr> 
  <tr> 
    <td>Henry V</td> 
    <td>History</td> 
    <td>1599</td> 
  </tr> 
</table> 
<h2>Shakespeare's Sonnets</h2> 
<table> 
  <tr> 
    <td>The Fair Youth</td> 
    <td>1-126</td> 
  </tr> 
  <tr> 
    <td>The Dark Lady</td> 
    <td>127-152</td> 
  </tr> 
  <tr> 
    <td>The Rival Poet</td> 
    <td>78-86</td> 
  </tr> 
</table> 

从我们的样式表中应用最小的样式后,这些标题和表格看起来相当普通。表格具有纯白色背景,没有样式区分一行和下一行,如下截图所示:

现在,我们可以向样式表中的所有表格行添加样式,并对奇数行使用alt类:

tr { 
  background-color: #fff;  
} 
.alt { 
  background-color: #ccc;  
} 

最后,我们编写我们的 jQuery 代码,将类附加到奇数行的表格行(<tr>标签):

$(() => { 
  $('tr:even').addClass('alt'); 
}); 

列表 2.7

但等等!为什么使用:even选择器来选择奇数行?好吧,就像使用:eq()选择器一样,:even:odd选择器使用 JavaScript 的本地从零开始的编号。因此,第一行计为零(偶数)和第二行计为一(奇数),依此类推。有了这一点,我们可以期望我们简单的代码生成如下所示的表格:

请注意,对于第二个表格,这个结果可能不是我们想要的。由于剧目表中最后一行具有交替的灰色背景,而十四行诗表中的第一行具有普通的白色背景。避免这种问题的一种方法是使用:nth-child()选择器,该选择器计算元素相对于其父元素的位置,而不是相对于到目前为止选择的所有元素的位置。此选择器可以使用数字、奇数偶数作为参数:

$(() => {
  $('tr:nth-child(odd)').addClass('alt'); 
}); 

列表 2.8

与之前一样,请注意:nth-child()是唯一一个以 1 为基础的 jQuery 选择器。为了实现与之前相同的行条纹效果--但对于第二个表格具有一致的行为,我们需要使用奇数而不是偶数作为参数。使用此选择器后,两个表格现在都有很好的条纹,如下截图所示:

:nth-child()选择器是现代浏览器中本机的 CSS 选择器。

基于文本内容查找元素

对于最后一个自定义选择器,假设出于某种原因,我们希望突出显示任何一个表格单元格,该单元格提到了亨利的剧目。我们只需--在样式表中添加一个使文本加粗和斜体的类(.highlight {font-weight:bold; font-style: italic;})--在我们的 jQuery 代码中使用:contains()选择器添加一行:

$(() => { 
  $('tr:nth-child(odd)')
    .addClass('alt'); 
  $('td:contains(Henry)')
    .addClass('highlight'); 
}); 

列表 2.9

因此,现在我们可以看到我们可爱的带有亨利剧集的条纹表格突出显示:

需要注意的是,:contains() 选择器区分大小写。使用不带大写 "H" 的 $('td:contains(henry)') 将不选择任何单元格。还需要注意的是,:contains() 可能会导致灾难性的性能下降,因为需要加载匹配第一部分选择器的每个元素的文本,并将其与我们提供的参数进行比较。当 :contains() 有可能搜索数百个节点以查找内容时,是时候重新考虑我们的方法了。

诚然,有多种方法可以实现行条纹和文本突出显示,而不需要 jQuery,或者说,根本不需要客户端编程。尽管如此,在动态生成内容且我们无法访问 HTML 或服务器端代码的情况下,jQuery 与 CSS 是这种类型样式的绝佳选择。

表单选择器

自定义选择器的功能不仅限于根据位置定位元素。例如,在处理表单时,jQuery 的自定义选择器和补充的 CSS3 选择器可以轻松选择我们需要的元素。以下表格描述了其中一些表单选择器:

选择器 匹配
:input 输入、文本区域、选择器和按钮元素
:button 按钮元素和带有 type 属性等于 button 的输入元素
:enabled 已启用的表单元素
:disabled 已禁用的表单元素
:checked 已选中的单选按钮或复选框
:selected 已选择的选项元素

与其他选择器一样,表单选择器可以组合使用以提高特异性。例如,我们可以选择所有已选中的单选按钮(但不包括复选框):$('input[type="radio"]:checked'),或选择所有密码输入和禁用的文本输入:$('input[type="password"], input[type="text"]:disabled')。即使使用自定义选择器,我们也可以使用相同的基本 CSS 原理来构建匹配元素列表。

我们在这里仅仅触及了可用选择器表达式的皮毛。我们将在第九章,高级选择器和遍历中深入探讨这个主题。

DOM 遍历方法

到目前为止,我们探索的 jQuery 选择器允许我们在 DOM 树中向下导航并过滤结果,如果这是选择元素的唯一方式,我们的选择会受到一定限制。在许多情况下,选择父元素或祖先元素至关重要;这就是 jQuery 的 DOM 遍历方法发挥作用的地方。使用这些方法,我们可以轻松地在 DOM 树中向上、向下和周围移动。

一些方法在选择器表达式中具有几乎相同的对应项。例如,我们首先用来添加alt类的行,$('tr:even').addClass('alt'),可以使用.filter()方法重写如下:

$('tr')
  .filter(':even')
  .addClass('alt'); 

然而,在很大程度上,这两种选择元素的方式互补。此外,特别是.filter()方法具有巨大的威力,因为它可以将函数作为其参数。该函数允许我们为是否应将元素包含在匹配的集合中创建复杂的测试。例如,假设我们想要为所有外部链接添加一个类:

a.external { 
  background: #fff url(images/external.png) no-repeat 100% 2px; 
  padding-right: 16px; 
} 

jQuery 没有这种选择器。如果没有过滤函数,我们将被迫显式地遍历每个元素,分别测试每个元素。但是,有了下面的过滤函数,我们仍然可以依赖于 jQuery 的隐式迭代,并保持我们的代码简洁:

$('a')
  .filter((i, a) =>
    a.hostname && a.hostname !== location.hostname
  )
  .addClass('external'); 

列表 2.10

提供的函数通过两个标准筛选<a>元素集:

  • 链接必须具有域名(a.hostname)的href属性。我们使用此测试来排除邮件链接,例如。

  • 它们链接到的域名(再次,a.hostname)不得与当前页面的域名(location.hostname)匹配。

更精确地说,.filter()方法遍历匹配的元素集,每次调用函数并测试返回值。如果函数返回false,则从匹配的集合中删除该元素。如果返回true,则保留该元素。

使用.filter()方法后,Henry V 链接被设置为外部链接的样式:

在下一节中,我们将再次查看我们条纹表格示例,看看遍历方法还有什么其他可能性。

样式化特定单元格

早些时候,我们向所有包含文本 Henry 的单元格添加了highlight类。要改为样式化每个包含 Henry 的单元格旁边的单元格,我们可以从已经编写的选择器开始,并简单地在结果上调用.next()方法:

$(() => {
  $('td:contains(Henry)')
    .next()
    .addClass('highlight'); 
}); 

列表 2.11

现在表格应该是这样的:

.next()方法仅选择紧接的下一个同级元素。要突出显示包含 Henry 的单元格后面的所有单元格,我们可以改用.nextAll()方法:

$(() => {
  $('td:contains(Henry)')
    .nextAll()
    .addClass('highlight'); 
}); 

列表 2.12

由于包含 Henry 的单元格位于表格的第一列中,此代码会导致这些行中的其余单元格被突出显示:

正如我们可能预期的那样,.next().nextAll()方法有对应的方法:.prev().prevAll()。此外,.siblings()选择同一 DOM 级别的所有其他元素,无论它们是在之前还是之后选择的元素之后。

要包含原始单元格(包含 Henry 的单元格)以及随后的单元格,我们可以添加.addBack()方法:

$(() => {
  $('td:contains(Henry)')
    .nextAll()
    .addBack() 
    .addClass('highlight'); 
}); 

列表 2.13

使用这个修改后,该行中的所有单元格都从highlight类中获取其样式:

我们可以通过多种选择器和遍历方法的组合来选择相同的元素集。例如,这里是另一种选择每行中至少一个单元格包含 Henry 的方法:

$(() => { 
  $('td:contains(Henry)')
    .parent()
    .children() 
    .addClass('highlight'); 
}); 

列表 2.14

我们不是沿着兄弟元素遍历,而是在 DOM 中向上移动到带有 .parent()<tr> 标记,然后用 .children() 选择所有行的单元格。

链式调用

我们刚刚探索过的遍历方法组合展示了 jQuery 的链式调用能力。使用 jQuery,可以在一行代码中选择多个元素集并对其执行多个操作。这种链式调用不仅有助于保持 jQuery 代码简洁,而且在替代重新指定选择器的情况下,还可以改善脚本的性能。

链式调用的工作原理

几乎所有的 jQuery 方法都会返回一个 jQuery 对象,因此可以对结果应用更多的 jQuery 方法。我们将在第八章中探讨链式调用的内部工作原理,开发插件

为了提高可读性,也可以将一行代码分成多行。例如,在本章中我们一直在做的就是这样。例如,一个单独的链式方法序列可以写在一行中:

$('td:contains(Henry)').parent().find('td:eq(1)') 
    .addClass('highlight').end().find('td:eq(2)') 
                           .addClass('highlight'); 

列表 2.15

这些方法的顺序也可以用七行来写:

$('td:contains(Henry)') // Find every cell containing "Henry" 
  .parent() // Select its parent 
  .find('td:eq(1)') // Find the 2nd descendant cell 
  .addClass('highlight') // Add the "highlight" class 
  .end() // Return to the parent of the cell containing "Henry" 
  .find('td:eq(2)') // Find the 3rd descendant cell 
  .addClass('highlight'); // Add the "highlight" class 

列表 2.16

此示例中的 DOM 遍历是刻意的,不建议使用。我们可以清楚地看到,我们可以使用更简单、更直接的方法。这个例子的重点只是展示了链式调用给我们带来的巨大灵活性,特别是当需要进行多次调用时。

链式调用就像在一个呼吸里说完整个段落的话语一样——可以快速完成工作,但对于其他人来说可能很难理解。将其分成多行并添加适当的注释可以在长远来看节省更多时间。

迭代 jQuery 对象

jQuery 3 中的新功能是使用 for...of 循环迭代 jQuery 对象。这本身并不是什么大不了的事情。首先,我们很少需要明确地迭代 jQuery 对象,特别是当使用 jQuery 函数中的隐式迭代也能得到相同的结果时。但有时,无法避免明确迭代。例如,想象一下你需要将一个元素数组(一个 jQuery 对象)减少为一个字符串值数组。each() 函数在这里是一种选择:

const eachText = [];

$('td')
  .each((i, td) => {
    if (td.textContent.startsWith('H')) {
      eachText.push(td.textContent);
    }
  });

console.log('each', eachText);
 // ["Hamlet", "Henry IV, Part I", "History", "Henry V", "History"]

列表 2.17

我们首先用 $('td') 选择器得到了一个 <td> 元素数组。然后,通过将 each() 函数传递一个回调来将每个以 "H" 开头的字符串推到 eachText 数组中,我们将其减少为一个字符串数组。这种方法没有问题,但是为这样一个简单的任务编写回调函数似乎有点过分了。下面是使用 for...of 语法实现相同功能的代码:

 const forText = [];

 for (let td of $('td')) {
   if (td.textContent.startsWith('H')) {
     forText.push(td.textContent);
   }
 }

 console.log('for', forText);
 // ["Hamlet", "Henry IV, Part I", "History", "Henry V", "History"]

列表 2.18

通过简单的for循环和if语句,我们现在可以缩减 jQuery 对象。我们将在本书后面重新讨论这种for...of方法,以适用更高级的使用场景,包括生成器。

访问 DOM 元素

每个选择器表达式和大多数 jQuery 方法都返回一个 jQuery 对象。这几乎总是我们想要的,因为它提供了隐式迭代和链接的功能。

然而,在我们的代码中可能会有一些情况需要直接访问 DOM 元素。例如,我们可能需要使生成的元素集合可供其他 JavaScript 库使用,或者可能需要访问元素的标签名称,这作为 DOM 元素的一个属性可用。对于这些明显罕见的情况,jQuery 提供了.get()方法。例如,要访问 jQuery 对象引用的第一个 DOM 元素,我们会使用.get(0)。因此,如果我们想要知道 ID 为my-element的元素的标签名称,我们会这样写:

$('#my-element').get(0).tagName; 

为了更加便利,jQuery 提供了.get()的简写。我们可以直接在选择器后面使用方括号来代替前面的行:

$('#my-element')[0].tagName; 

这种语法看起来像是将 jQuery 对象视为 DOM 元素的数组并不是偶然的;使用方括号就像是把 jQuery 层剥离出去,得到节点列表,并包括索引(在这种情况下,0),就像是取出 DOM 元素本身。

总结

通过本章介绍的技巧,现在我们应该能够以各种方式在页面上定位元素集合。特别是,我们学习了如何使用基本的 CSS 选择器来为嵌套列表的顶层和子层项目设置样式,如何使用属性选择器为不同类型的链接应用不同的样式,如何使用自定义的 jQuery 选择器:odd:even或高级 CSS 选择器:nth-child()为表格添加基本的条纹,并通过链接 jQuery 方法来突出显示特定表格单元格中的文本。

到目前为止,我们一直在使用$(() => {})文档准备处理程序来给匹配的元素集合添加类。在下一章中,我们将探讨在响应各种用户触发事件中添加类的方法。

进一步阅读

选择器和遍历方法的主题将在第九章《高级选择器和遍历》中更详细地探讨。jQuery 的选择器和遍历方法的完整列表可在本书的附录 B 中找到,也可在官方的 jQuery 文档api.jquery.com/中找到。

练习

挑战练习可能需要使用官方的 jQuery 文档 api.jquery.com/

  1. 为嵌套列表的第二级所有<li>元素添加一个special类。

  2. 为表格的第三列中的所有单元格添加一个year类。

  3. 在含有单词Tragedy的第一行表格行中添加special类。

  4. 这里有一个挑战给你。选择所有包含链接(<a>)的列表项(<li>)。给所选项后面的兄弟列表项添加类afterlink

  5. 这里有另一个挑战给你。给任何.pdf链接最近的祖先<ul>添加类tragedy

第三章:处理事件

JavaScript 有几种内置的方式来响应用户交互和其他事件。为了使页面动态和响应灵活,我们需要利用这种能力,以便在适当的时候使用你迄今为止学到的 jQuery 技巧和你以后将学到的其他技巧。虽然我们可以用原生 JavaScript 来做到这一点,但 jQuery 增强和扩展了基本的事件处理机制,使其具有更优雅的语法,同时使其更加强大。

在本章中,我们将涵盖:

  • 当页面准备就绪时执行 JavaScript 代码

  • 处理用户事件,如鼠标点击和按键

  • 事件通过文档的流动,以及如何操纵该流动

  • 模拟事件,就像用户发起了它们一样

在页面加载时执行任务

我们已经看到如何使 jQuery 响应网页加载。 $(() => {}) 事件处理程序可用于运行依赖于 HTML 元素的代码,但还有一些其他内容需要讨论。

代码执行的时间

在第一章中,入门,我们注意到 $(() => {}) 是 jQuery 在页面加载时执行任务的主要方式。然而,这并不是我们唯一的选择。本地的 window.onload 事件也可以做同样的事情。虽然这两种方法相似,但重要的是要认识到它们在时间上的差异,尤其是依赖于加载的资源数量的情况下,这可能是相当微妙的。

当文档完全下载到浏览器时,window.onload 事件将触发。这意味着页面上的每个元素都可以被 JavaScript 操纵,这对于编写功能丰富的代码而不用担心加载顺序是一个福音。

另一方面,使用 $(() => {}) 注册的处理程序在 DOM 完全准备就绪时被调用。这也意味着所有元素都可以被我们的脚本访问,但并不意味着每个相关文件都已经被下载。一旦 HTML 文件被下载并解析成 DOM 树,代码就可以运行。

样式加载和代码执行

为了确保页面在 JavaScript 代码执行之前也已经被样式化,将 <link rel="stylesheet"><style> 标签放在文档的 <head> 元素内的任何 <script> 标签之前是一种良好的做法。

例如,考虑一个展示图库的页面;这样的页面上可能有许多大图,我们可以用 jQuery 隐藏、显示、移动和其他方式来操纵它们。如果我们使用 onload 事件来设置我们的接口,用户将不得不等待每个图像完全下载后才能使用这些功能。更糟糕的是,如果行为尚未附加到具有默认行为的元素(如链接)上,用户交互可能会产生意想不到的结果。然而,当我们使用 $(() => {}) 进行设置时,界面更早地准备好使用,并具有正确的行为。

什么被加载了,什么没有被加载?

使用$(() => {})几乎总是优于使用onload处理程序,但我们需要记住,因为支持文件可能尚未加载,因此此时可能不一定可用图像高度和宽度等属性。如果需要这些属性,有时我们也可以选择实现onload处理程序;这两种机制可以和平共处。

处理一个页面上的多个脚本

通过 JavaScript 注册事件处理程序的传统机制(而不是直接在 HTML 内容中添加处理程序属性)是将函数分配给 DOM 元素的相应属性。例如,假设我们已定义了以下函数:

function doStuff() { 
  // Perform a task... 
} 

然后,我们可以在 HTML 标记中分配它:

<body onl oad="doStuff();"> 

或者,我们可以从 JavaScript 代码中分配它:

window.onload = doStuff; 

这两种方法都会在页面加载时执行函数。第二种的优点是行为与标记清晰地分开。

引用与调用函数

当我们将函数分配为处理程序时,我们使用函数名但省略尾括号。带括号时,函数会立即调用;不带括号时,名称仅标识或引用函数,并且可以在以后调用它。

通过一个函数,这种策略运行得相当不错。然而,假设我们有一个第二个函数如下:

function doOtherStuff() { 
  // Perform another task... 
} 

然后,我们可以尝试将此函数分配为在页面加载时运行:

window.onload = doOtherStuff; 

但是,这个赋值会覆盖第一个。.onload属性一次只能存储一个函数引用,所以我们不能添加到现有的行为。

$(() => {})机制优雅地处理了这种情况。每次调用都会将新函数添加到内部行为队列中;当页面加载时,所有函数都将执行。函数将按照注册的顺序运行。

公平地说,jQuery 并不是唯一解决此问题的方法。我们可以编写一个 JavaScript 函数,调用现有的onload处理程序,然后调用传入的处理程序。这种方法避免了像$(() => {})这样的竞争处理程序之间的冲突,但缺少了我们讨论过的其他一些优点。在现代浏览器中,可以使用 W3C 标准的document.addEventListener()方法触发DOMContentLoaded事件。但是,$(() => {})更简洁而优雅。

将参数传递给文档准备好的回调

在某些情况下,同时在同一页面上使用多个 JavaScript 库可能会被证明是有用的。由于许多库使用$标识符(因为它简短而方便),我们需要一种方法来防止库之间的冲突。

幸运的是,jQuery 提供了一个名为jQuery.noConflict()的方法,将$标识符的控制权返回给其他库。jQuery.noConflict()的典型用法如下所示:

<script src="img/prototype.js"></script> 
<script src="img/jquery.js"></script> 
<script> 
  jQuery.noConflict(); 
</script> 
<script src="img/myscript.js"></script> 

首先,包括其他库(本例中的prototype.js)。然后,jquery.js自身被包括,接管$以供自己使用。接下来,调用.noConflict()释放$,以便将其控制权恢复到第一个包括的库(prototype.js)。现在在我们的自定义脚本中,我们可以同时使用这两个库,但每当我们想使用 jQuery 方法时,我们需要将标识符写为jQuery而不是$

$(() => {}) 文档准备就绪处理程序在这种情况下还有一个技巧可以帮助我们。我们传递给它的回调函数可以接受一个参数--jQuery对象本身。这使我们可以有效地重新命名它,而不必担心冲突,使用以下语法:

jQuery(($) => { 
  // In here, we can use $ like normal! 
}); 

处理简单事件

除了页面加载之外,还有其他时间点,我们可能希望执行某些任务。就像 JavaScript 允许我们拦截页面加载事件一样,使用<body onl oad="">window.onload,它为用户触发的事件提供了类似的挂钩,如鼠标点击(onclick)、表单字段被修改(onchange)和窗口尺寸变化(onresize)。当直接分配给 DOM 中的元素时,这些挂钩也具有类似于我们为onload概述的缺点。因此,jQuery 也提供了处理这些事件的改进方式。

一个简单的样式切换器

为了说明一些事件处理技术,假设我们希望根据用户输入以多种不同的样式呈现单个页面;我们将提供按钮,允许用户在正常视图、文本受限于窄列的视图和内容区域为大字体的视图之间切换。

逐步增强

在一个真实的例子中,一个良好的网络公民将在这里应用逐步增强原则。在第五章,操作 DOM中,您将学到如何可以从我们的 jQuery 代码直接注入类似于这种样式切换器的内容,以便没有可用 JavaScript 的用户不会看到无效的控件。

样式切换器的 HTML 标记如下:

<div id="switcher" class="switcher"> 
  <h3>Style Switcher</h3> 
  <button id="switcher-default"> 
    Default 
  </button> 
  <button id="switcher-narrow"> 
    Narrow Column 
  </button> 
  <button id="switcher-large"> 
    Large Print 
  </button> 
</div> 

获取示例代码

您可以访问下面的 GitHub 存储库中的示例代码:github.com/PacktPublishing/Learning-jQuery-3

结合页面的其余 HTML 标记和一些基本的 CSS,我们得到了一个看起来像以下的页面:

首先,我们将让大字体按钮起作用。我们需要一些 CSS 来实现我们页面的另一种视图,如下所示:

body.large .chapter { 
  font-size: 1.5em; 
} 

因此,我们的目标是将large类应用于<body>标签。这将允许样式表适当地重新格式化页面。根据您在第二章学到的,选择元素,我们已经知道完成这个任务所需的语句:

$('body').addClass('large'); 

然而,我们希望这发生在按钮被点击时,而不是在页面加载时,就像我们迄今所见的那样。为此,我们将介绍.on()方法。该方法允许我们指定任何 DOM 事件并附加行为。在这种情况下,事件被称为click,而行为是由我们之前的一行函数组成:

$(() => {
  $('#switcher-large')
    .on('click', () => { 
      $('body').addClass('large'); 
    }); 
}); 

列表 3.1

现在当按钮被点击时,我们的代码运行,文字被放大:

这就是将行为绑定到事件的全部内容。我们讨论的$(() => {})文档就绪处理程序的优势在这里同样适用。多次调用.on()可以很好地共存,根据需要向同一事件附加附加行为。

这并不一定是实现此任务的最优雅或高效方式。随着我们继续学习本章,我们将扩展和完善这段代码,使之成为我们可以自豪的东西。

启用其他按钮

现在我们有了有效运行的大字按钮,但我们需要对其他两个按钮(默认和窄栏)应用类似的处理以使它们执行其任务。这很简单:我们使用.on()为每个按钮添加一个click处理程序,根据需要删除和添加类。新代码如下所示:

$(() => {
  $('#switcher-default')
    .on('click', () => { 
      $('body')
        .removeClass('narrow')
        .removeClass('large'); 
    });

  $('#switcher-narrow')
    .on('click', () => { 
      $('body')
        .addClass('narrow')
        .removeClass('large'); 
    }); 

  $('#switcher-large')
    .on('click', () => { 
      $('body')
        .removeClass('narrow')
        .addClass('large'); 
    }); 
}); 

列表 3.2

这与narrow类的 CSS 规则相结合:

body.narrow .chapter { 
  width: 250px; 
} 

现在,在点击"窄栏"按钮后,其相应的 CSS 被应用,文本布局不同了:

点击"Default"按钮会从<body>标签中移除两个类名,使页面恢复到最初的渲染状态。

利用事件处理程序上下文

我们的切换器行为正确,但我们没有向用户提供有关当前活动按钮的任何反馈。我们处理的方法是在点击时将selected类应用到按钮上,并从其他按钮上删除这个类。selected类只是使按钮的文字加粗:

.selected { 
  font-weight: bold; 
} 

我们可以像之前一样通过引用每个按钮的 ID 并根据需要应用或移除类来实现此类修改,而是,我们将探讨一种更加优雅和可扩展的解决方案,利用事件处理程序运行的上下文。

当任何事件处理程序被触发时,关键字this指代的是附加行为的 DOM 元素。早些时候我们注意到$()函数可以将 DOM 元素作为参数;这是为何该功能可用的关键原因之一。在事件处理程序中写入$(this),我们创建了一个对应于该元素的 jQuery 对象,我们可以像使用 CSS 选择器定位一样对其进行操作。

有了这个思路,我们可以写出以下内容:

$(this).addClass('selected'); 

在每个处理程序中放置这行代码会在按钮被点击时添加类。要从其他按钮中移除类,我们可以利用 jQuery 的隐式迭代功能,并写入:

$('#switcher button').removeClass('selected'); 

此行从样式切换器中的每个按钮中移除类。

当文档准备就绪时,我们还应该向默认按钮添加类。因此,将这些放置在正确的顺序中,代码如下所示:

$(() => { 
  $('#switcher-default') 
    .addClass('selected') 
    .on('click', function() { 
      $('body')
        .removeClass('narrow'); 
        .removeClass('large'); 
      $('#switcher button')
        .removeClass('selected'); 
      $(this)
        .addClass('selected'); 
    });

  $('#switcher-narrow')
    .on('click', function() { 
        $('body')
          .addClass('narrow')
          .removeClass('large'); 
        $('#switcher button')
          .removeClass('selected'); 
        $(this)
          .addClass('selected'); 
  }); 

  $('#switcher-large')
    .on('click', function() { 
      $('body')
        .removeClass('narrow')
        .addClass('large'); 
      $('#switcher button')
        .removeClass('selected'); 
      $(this)
        .addClass('selected'); 
  }); 
}); 

列表 3.3

现在样式切换器提供了适当的反馈。

通过使用处理程序上下文概括语句,我们可以更加高效。我们可以将突出显示的例程提取到单独的处理程序中,如列表 3.4所示,因为它对所有三个按钮都是相同的:

$(() => {
  $('#switcher-default') 
    .addClass('selected') 
    .on('click', function() { 
      $('body')
        .removeClass('narrow')
        .removeClass('large'); 
    }); 
  $('#switcher-narrow')
    .on('click', () => { 
      $('body')
        .addClass('narrow')
        .removeClass('large'); 
    }); 

  $('#switcher-large')
    .on('click', () => { 
      $('body')
        .removeClass('narrow')
        .addClass('large'); 
    }); 

  $('#switcher button')
    .on('click', function() { 
      $('#switcher button')
        .removeClass('selected'); 
      $(this)
        .addClass('selected'); 
    }); 
}); 

列表 3.4

这种优化利用了我们已经讨论过的三个 jQuery 功能。首先,当我们使用单个调用.on()将相同的click处理程序绑定到每个按钮时,隐式迭代再次非常有用。其次,行为排队允许我们将两个函数绑定到同一个点击事件,而不会第二个覆盖第一个。

当事件处理程序函数使用this引用其上下文时,你不能使用箭头函数(() => {})。这些函数具有词法上下文。这意味着当 jQuery 尝试将上下文设置为触发事件的元素时,它不起作用。

利用事件上下文合并代码

我们刚刚完成的代码优化是重构的一个例子--修改现有代码以以更高效或更优雅的方式执行相同的任务。为了进一步探索重构机会,让我们看一下我们已经绑定到每个按钮的行为。.removeClass()方法的参数是可选的;当省略时,它会从元素中删除所有类。我们可以利用这一点稍微简化我们的代码,如下所示:

$(() => {
  $('#switcher-default') 
    .addClass('selected') 
    .on('click', () => { 
      $('body').removeClass(); 
    });
  $('#switcher-narrow')
    .on('click', () => { 
      $('body')
        .removeClass()
        .addClass('narrow'); 
    }); 

  $('#switcher-large')
    .on('click', () => { 
      $('body')
        .removeClass()
        .addClass('large'); 
    }); 

  $('#switcher button')
    .on('click', function() { 
      $('#switcher button')
        .removeClass('selected'); 
      $(this)
        .addClass('selected'); 
    }); 
}); 

列表 3.5

请注意,操作顺序有些变化,以适应我们更一般的类移除;我们需要先执行.removeClass(),以免它撤消对.addClass()的调用,我们同时执行这个调用。

我们只能安全地删除所有类,因为在这种情况下我们负责 HTML。当我们编写可重用的代码(例如用于插件)时,我们需要尊重可能存在的任何类,并保持其不变。

现在我们在每个按钮的处理程序中执行一些相同的代码。这可以很容易地提取出来到我们的通用按钮click处理程序中:

$(() => {
  $('#switcher-default')
    .addClass('selected'); 
  $('#switcher button')
    .on('click', function() { 
      $('body')
        .removeClass(); 
      $('#switcher button')
        .removeClass('selected'); 
      $(this)
        .addClass('selected'); 
    });

  $('#switcher-narrow')
    .on('click', () => { 
      $('body')
        .addClass('narrow'); 
    }); 

  $('#switcher-large')
    .on('click', () => { 
      $('body')
        .addClass('large'); 
    }); 
}); 

列表 3.6

请注意,现在我们需要将通用处理程序移到特定处理程序之前。.removeClass()调用需要在.addClass()执行之前发生,我们可以依赖于此,因为 jQuery 总是按照注册顺序触发事件处理程序。

最后,我们可以完全摆脱特定的处理程序,再次利用事件上下文。由于上下文关键字this给了我们一个 DOM 元素而不是 jQuery 对象,我们可以使用原生 DOM 属性来确定被点击的元素的 ID。因此,我们可以将相同的处理程序绑定到所有按钮上,并在处理程序内为每个按钮执行不同的操作:

$(() => {
  $('#switcher-default')
    .addClass('selected'); 
  $('#switcher button')
    .on('click', function() { 
      const bodyClass = this.id.split('-')[1]; 
      $('body')
        .removeClass()
        .addClass(bodyClass); 
      $('#switcher button')
        .removeClass('selected'); 
      $(this)
        .addClass('selected'); 
    }); 
}); 

列表 3.7

bodyClass变量的值将是defaultnarrowlarge,具体取决于点击了哪个按钮。在这里,我们有些偏离了以前的代码;当用户单击<button id="switcher-default">时,我们为<body>添加了一个default类。虽然我们不需要应用这个类,但它也没有造成任何损害,代码复杂性的减少完全弥补了一个未使用的类名。

快捷事件

绑定事件处理程序(如简单的click事件)是一项非常常见的任务,jQuery 提供了一个更简洁的方法来完成它;快捷事件方法与它们的.on()对应方法以更少的击键方式工作。

例如,我们的样式切换器可以使用.click()而不是.on()来编写,如下所示:

$(() => {
  $('#switcher-default')
    .addClass('selected');

  $('#switcher button')
    .click(function() { 
      const bodyClass = this.id.split('-')[1]; 
      $('body')
        .removeClass()
        .addClass(bodyClass); 
      $('#switcher button')
        .removeClass('selected'); 
      $(this)
        .addClass('selected'); 
  }); 
}); 

列表 3.8

其他标准 DOM 事件(如blurkeydownscroll)也存在类似的快捷事件方法。每个快捷方法都会使用相应的名称将处理程序绑定到事件上。

显示和隐藏页面元素

假设我们希望在不需要时能够隐藏我们的样式切换器。隐藏页面元素的一种方便方法是使它们可折叠。我们将允许单击标签一次来隐藏按钮,只留下标签。再次单击标签将恢复按钮。我们需要另一个类来隐藏按钮:

.hidden { 
  display: none; 
} 

我们可以通过将按钮的当前状态存储在变量中,并在每次单击标签时检查其值,以了解是否应在按钮上添加或删除隐藏类来实现此功能。然而,jQuery 为我们提供了一种简单的方法,根据该类是否已经存在来添加或删除一个类——.toggleClass()方法:

$(() => {
  $('#switcher h3')
    .click(function() {
      $(this)
        .siblings('button')
        .toggleClass('hidden');
    });
}); 

列表 3.9

第一次点击后,所有按钮都被隐藏了:

第二次点击然后将它们恢复到可见状态:

再次,我们依赖隐式迭代,这次是为了一举隐藏所有按钮——<h3>的兄弟节点。

事件传播

为了说明click事件能够作用于通常不可点击的页面元素的能力,我们制作了一个界面,没有显示出样式切换器标签——只是一个<h3>元素——实际上是页面中等待用户交互的活动部分。为了纠正这一点,我们可以给它一个鼠标悬停状态,清楚地表明它以某种方式与鼠标交互:

.hover { 
  cursor: pointer; 
  background-color: #afa; 
} 

CSS 规范包括一个名为:hover的伪类,允许样式表在用户鼠标悬停在元素上时影响其外观。这在这种情况下肯定可以解决我们的问题,但是我们将利用这个机会介绍 jQuery 的.hover()方法,它允许我们使用 JavaScript 来改变元素的样式——事实上,执行任意操作——当鼠标光标进入元素时和离开元素时。

.hover()方法接受两个函数参数,与我们迄今为止遇到的简单事件方法不同。第一个函数将在鼠标光标进入所选元素时执行,第二个函数将在鼠标离开时执行。我们可以修改这些时间应用的类来实现鼠标悬停效果:

$(() => { 
  $('#switcher h3')
    .hover(function() { 
      $(this).addClass('hover'); 
    }, function() { 
      $(this).removeClass('hover'); 
    }); 
}); 

列表 3.10

我们再次使用隐式迭代和事件上下文来编写简短简单的代码。现在当鼠标悬停在<h3>元素上时,我们看到我们的类被应用了:

使用.hover()还意味着我们避免了 JavaScript 中事件传播引起的头痛。要理解这一点,我们需要看一下 JavaScript 如何决定哪个元素可以处理给定事件。

事件的旅程

当页面上发生事件时,整个 DOM 元素层次结构都有机会处理事件。考虑以下页面模型:

<div class="foo"> 
  <span class="bar"> 
    <a href="http://www.example.com/"> 
      The quick brown fox jumps over the lazy dog. 
    </a> 
  </span> 
  <p> 
    How razorback-jumping frogs can level six piqued gymnasts! 
  </p> 
</div> 

然后我们将代码可视化为一组嵌套元素:

对于任何事件,逻辑上都可能负责响应的多个元素。例如,当单击此页面上的链接时,<div><span><a>元素都应该有机会响应单击事件。毕竟,这三个元素都在用户鼠标指针下。另一方面,<p>元素根本不参与这个交互。

一种允许多个元素响应用户交互的策略称为事件捕获。使用事件捕获,事件首先传递给最全面的元素,然后逐渐传递给更具体的元素。在我们的示例中,这意味着首先传递事件给<div>元素,然后是<span>元素,最后是<a>元素,如下图所示:

相反的策略称为事件冒泡。事件被发送到最具体的元素,然后在此元素有机会响应后,事件向更一般的元素冒泡。在我们的示例中,<a>元素将首先处理事件,然后按顺序是<span><div>元素,如下图所示:

毫不奇怪,不同的浏览器开发者最初决定了不同的事件传播模型。最终开发的 DOM 标准规定应该同时使用这两种策略:首先从一般元素捕获事件到特定元素,然后事件冒泡回 DOM 树的顶部。可以为此过程的任一部分注册事件处理程序。

为了提供一致且易于理解的行为,jQuery 始终为模型的冒泡阶段注册事件处理程序。我们始终可以假设最具体的元素将首先有机会响应任何事件。

事件冒泡的副作用

事件冒泡可能会导致意外行为,特别是当错误的元素响应mouseovermouseout事件时。考虑一个附加到我们示例中的<div>元素的mouseout事件处理程序。当用户的鼠标光标退出<div>元素时,mouseout处理程序按预期运行。由于这是在层次结构的顶部,没有其他元素获得事件。另一方面,当光标退出<a>元素时,mouseout事件被发送到该元素。然后,此事件将冒泡到<span>元素,然后到<div>元素,触发相同的事件处理程序。这种冒泡序列可能不是期望的。

mouseentermouseleave事件,无论是单独绑定还是结合在.hover()方法中,都意识到了这些冒泡问题,当我们使用它们来附加事件时,我们可以忽略由于错误的元素获取mouseovermouseout事件而引起的问题。

刚刚描述的mouseout场景说明了限制事件范围的必要性。虽然.hover()处理了这种特殊情况,但我们将遇到其他需要在空间上(防止将事件发送到某些元素)或在时间上(在某些时间阻止事件发送)限制事件的情况。

改变旅程 - 事件对象

我们已经看到了一种情况,其中事件冒泡可能会引起问题。为了展示一种情况,.hover()不能帮助我们的情况,我们将更改我们之前实现的折叠行为。

假设我们希望扩大点击区域,触发样式切换器的折叠或展开。一种方法是将事件处理程序从标签<h3>移动到其包含的<div>元素中。在列表 3.9中,我们向#switcher h3添加了一个click处理程序;我们将尝试通过将处理程序附加到#switcher而不是附加到#switcher来进行此更改:

$(() => {
  $('#switcher')
    .click(() => {
      $('#switcher button').toggleClass('hidden'); 
    }); 
}); 

列表 3.11

这种改变使得整个样式切换器区域都可点击以切换其可见性。缺点是点击按钮后,样式切换器也会折叠,这是因为事件冒泡;事件首先由按钮处理,然后通过 DOM 树传递,直到达到<div id="switcher">元素,在那里我们的新处理程序被激活并隐藏按钮。

要解决这个问题,我们需要访问event对象。这是一个传递给每个元素事件处理程序的 DOM 构造,当它被调用时。它提供了有关事件的信息,比如鼠标光标在事件发生时的位置。它还提供了一些可以用来影响事件在 DOM 中的进展的方法。

事件对象参考

有关 jQuery 对事件对象及其属性的实现的详细信息,请参见api.jquery.com/category/events/event-object/

要在处理程序中使用事件对象,我们只需要向函数添加一个参数:

$(() => {
  $('#switcher')
    .click(function(event) { 
      $('#switcher button').toggleClass('hidden'); 
    }); 
}); 

请注意,我们将此参数命名为event是因为它具有描述性,而不是因为我们需要。将其命名为flapjacks或其他任何东西都可以正常工作。

事件目标

现在我们可以在处理程序中使用事件对象作为event。属性event.target可以帮助我们控制事件生效的位置。这个属性是 DOM API 的一部分,但在一些较旧的浏览器版本中没有实现;jQuery 根据需要扩展事件对象,以在每个浏览器中提供这个属性。通过.target,我们可以确定 DOM 中的哪个元素首先接收到事件。对于click事件,这将是实际点击的项。记住this给我们提供了处理事件的 DOM 元素,我们可以编写以下代码:

$(() => {
  $('#switcher')
    .click(function(event) {
      if (event.target == this) { 
        $(this)
          .children('button')
          .toggleClass('hidden'); 
      } 
    }); 
}); 

列表 3.12

此代码确保所点击的项目是<div id="switcher">,而不是其子元素之一。现在,点击按钮将不会使样式切换器折叠,但点击切换器的背景。然而,点击标签<h3>现在不起作用,因为它也是一个子元素。我们可以修改按钮的行为来达到我们的目标,而不是在这里放置这个检查。

阻止事件传播

事件对象提供了.stopPropagation()方法,它可以完全停止事件的冒泡过程。像.target一样,这个方法是基本的 DOM 特性,但使用 jQuery 实现会隐藏我们代码中的任何浏览器不一致性。

我们将删除我们刚刚添加的event.target == this检查,并在我们的按钮的click处理程序中添加一些代码:

$(() => {
  $('#switcher')
    .click((e) => {
      $(e.currentTarget)
        .children('button')
        .toggleClass('hidden'); 
    }); 
}); 
$(() => {
  $('#switcher-default')
    .addClass('selected'); 
  $('#switcher button')
    .click((e) => { 
      const bodyClass = e.target.id.split('-')[1]; 

      $('body')
        .removeClass()
        .addClass(bodyClass); 
      $(e.target)
        .addClass('selected')
        .removeClass('selected'); 

      e.stopPropagation(); 
    }); 
}); 

列表 3.13

与以前一样,我们需要在我们用作click处理程序的函数中添加一个事件参数:e。然后,我们只需调用e.stopPropagation()来防止任何其他 DOM 元素对事件作出响应。现在我们的点击由按钮处理,只有按钮;在样式切换器的任何其他地方点击都会使其折叠或展开。

防止默认操作

如果我们的click事件处理程序是在一个链接元素(<a>)上注册的,而不是在一个表单之外的通用<button>元素上,我们将面临另一个问题。当用户点击链接时,浏览器会加载一个新页面。这种行为不是我们讨论的事件处理程序的一部分;相反,这是单击链接元素的默认操作。同样,当用户在编辑表单时按下Enter键时,可能会在表单上触发submit事件,但此后实际上会发生表单提交。

如果这些默认操作是不希望的,调用事件上的.stopPropagation()将无济于事。这些操作不会发生在事件传播的正常流程中。相反,.preventDefault()方法用于在触发默认操作之前立即停止事件。

在对事件环境进行一些测试后调用.preventDefault()通常是有用的。例如,在表单提交期间,我们可能希望检查必填字段是否已填写,并且仅在它们未填写时阻止默认操作。对于链接,我们可以在允许href被跟踪之前检查某些前提条件,从本质上讲,在某些情况下禁用链接。

事件传播和默认操作是独立的机制;其中之一可以在另一个发生时停止。如果我们希望同时停止两者,我们可以在事件处理程序的末尾返回false,这是对事件同时调用.stopPropagation().preventDefault()的快捷方式。

事件委托

事件冒泡并不总是一种阻碍;我们经常可以将其利用到极大的好处中。利用冒泡的一种伟大技术称为事件委托。通过它,我们可以使用单个元素上的事件处理程序来完成许多工作。

在我们的示例中,只有三个带有附加click处理程序的<button>元素。但是如果有多于三个呢?这比你想象的更常见。例如,考虑一个包含每行都有一个需要click处理程序的交互项的大型信息表格。隐式迭代使得分配所有这些click处理程序变得容易,但性能可能会因为 jQuery 内部的循环和维护所有处理程序的内存占用而受到影响。

相反,我们可以将单个click处理程序分配给 DOM 中的祖先元素。由于事件冒泡,无间断的click事件最终将到达祖先元素,我们可以在那里完成我们的工作。

举个例子,让我们将这种技术应用于我们的样式切换器(即使项目数量不需要这种方法)。如前面所见的清单 3.12,我们可以使用e.target属性来检查在发生click事件时鼠标光标下的哪个元素。

$(() => { 
  $('#switcher')
    .click((e) => {
      if ($(event.target).is('button')) { 
        const bodyClass = e.target.id.split('-')[1]; 

        $('body')
          .removeClass()
          .addClass(bodyClass); 
        $(e.target)
          .addClass('selected')
          .removeClass('selected'); 

        e.stopPropagation(); 
      }   
    }); 
}); 

清单 3.14

我们在这里使用了一个新方法叫做.is()。该方法接受我们在上一章中研究的选择器表达式,并测试当前 jQuery 对象是否与选择器匹配。如果集合中至少有一个元素与选择器匹配,.is()将返回true。在这种情况下,$(e.target).is('button')询问被点击的元素是否是一个<button>元素。如果是,我们将继续以前的代码,但有一个重大变化:关键字this现在指的是<div id="switcher">,所以每次我们感兴趣的是点击的按钮时,现在必须使用e.target来引用它。

.is() 和 .hasClass()

我们可以使用.hasClass()测试元素上类的存在。然而,.is()方法更灵活,可以测试任何选择器表达式。

但是,从这段代码中我们还有一个意外的副作用。现在,当单击按钮时,切换器会折叠,就像我们在添加调用.stopPropagation()之前的情况一样。切换器可见性切换器的处理程序现在绑定到与按钮的处理程序相同的元素上,因此停止事件冒泡不会阻止切换器触发。为了避开这个问题,我们可以删除.stopPropagation()调用,并且改为添加另一个.is()测试。另外,由于我们使整个切换器<div>元素可点击,所以应该在用户的鼠标位于其任何部分时切换hover类:

$(() => {
  const toggleHover = (e) => {
    $(e.target).toggleClass('hover');
  };

  $('#switcher')
    .hover(toggleHover, toggleHover);
});

$(() => {
  $('#switcher')
    .click((e) => {
      if (!$(e.target).is('button')) {
        $(e.currentTarget)
          .children('button')
          .toggleClass('hidden');
      }
    });
});

$(() => {
  $('#switcher-default')
    .addClass('selected');
  $('#switcher')
    .click((e) => {
      if ($(e.target).is('button')) {
        const bodyClass = e.target.id.split('-')[1];

        $('body')
          .removeClass()
          .addClass(bodyClass);
        $(e.target)
          .addClass('selected')
          .siblings('button')
          .removeClass('selected');
      }
  });
});

清单 3.15

这个例子在大小上有点复杂了,但是随着具有事件处理程序的元素数量的增加,事件委托的好处也会增加。此外,通过组合两个click处理程序并使用单个if-else语句进行.is()测试,我们可以避免一些代码重复:

$(() => {
  $('#switcher-default')
    .addClass('selected'); 
  $('#switcher')
    .click((e) => {
      if ($(e.target).is('button')) { 
        const bodyClass = e.target.id.split('-')[1]; 
        $('body')
          .removeClass()
          .addClass(bodyClass); 
        $(e.target)
          .addClass('selected')
          .removeClass('selected'); 
      } else { 
        $(e.currentTarget)
          .children('button')
          .toggleClass('hidden'); 
      } 
    }); 
}); 

清单 3.16

虽然我们的代码仍然需要一些微调,但它已经接近我们可以放心使用它的状态了。尽管如此,为了更多地了解 jQuery 的事件处理,我们将回到 清单 3.15 并继续修改该版本的代码。

事件委托在我们稍后会看到的其他情况下也很有用,比如当通过 DOM 操作方法(第五章,操作 DOM)或 Ajax 例程(第六章,使用 Ajax 发送数据)添加新元素时。

使用内置的事件委托功能

因为事件委托在很多情况下都很有用,jQuery 包含了一组工具来帮助开发者使用这个技术。我们已经讨论过的.on()方法可以在提供适当参数时执行事件委托:

$(() => {
  $('#switcher-default')
    .addClass('selected');
  $('#switcher')
   .on('click', 'button', (e) => {
     const bodyClass = e.target.id.split('-')[1];

     $('body')
       .removeClass()
       .addClass(bodyClass);
     $(e.target)
       .addClass('selected')
       .siblings('button')
       .removeClass('selected');

     e.stopPropagation();
   })
   .on('click', (e) => {
     $(e.currentTarget)
       .children('button')
       .toggleClass('hidden');
   });
});

清单 3.17

现在看起来很不错了。对于切换器功能中的所有点击事件,我们有两个非常简单的处理程序。我们在.on()方法中添加了一个选择器表达式作为第二个参数。具体来说,我们要确保将点击事件上升到#switch的任何元素实际上都是按钮元素。这比在事件处理程序中编写一堆逻辑来根据生成它的元素处理事件更好。

我们确实不得不添加一个调用e.stopPropagation()的方法。原因是为了使第二个点击处理程序,即处理切换按钮可见性的处理程序,无需担心检查事件来自何处。通常防止事件传播比在事件处理程序代码中引入边缘情况处理更容易。

经过一些小的折衷,我们现在有了一个单一的按钮点击处理函数,它可以处理 3 个按钮,也可以处理 300 个按钮。就是这些小细节使得 jQuery 代码能够很好地扩展。

我们将在第十章,高级事件中全面讨论.on()的使用。

移除事件处理程序

有时候我们会完成之前注册的事件处理程序。也许页面的状态已经改变,使得这个动作不再合理。我们可以在事件处理程序内部使用条件语句处理这种情况,但是完全解绑处理程序会更加优雅。

假设我们希望我们的可折叠样式切换器在页面不使用正常样式时保持展开。当选择窄列或大号字按钮时,单击样式切换器的背景应该没有任何效果。我们可以通过调用 .off() 方法来在点击非默认样式切换器按钮时移除折叠处理程序来实现这一点:

$(() => {
  $('#switcher')
    .click((e) => {
      if (!$(e.target).is('button')) {
        $(e.currentTarget)
          .children('button')
          .toggleClass('hidden');
      }
    });
  $('#switcher-narrow, #switcher-large')
    .click(() => {
      $('#switcher').off('click');
    });
});

图 3.18

现在当单击诸如窄列之类的按钮时,样式切换器 <div> 上的 click 处理程序被移除,单击框的背景不再使其折叠。然而,按钮不再起作用!它们也受到样式切换器 <div>click 事件影响,因为我们重写了按钮处理代码以使用事件委托。这意味着当我们调用 $('#switcher').off('click') 时,两种行为都被移除。

给事件处理程序命名空间

我们需要使我们的 .off() 调用更加具体,以便不移除我们注册的两个点击处理程序。一种方法是使用事件 命名空间。当事件绑定时,我们可以引入附加信息,以便稍后识别特定的处理程序。要使用命名空间,我们需要返回到绑定事件处理程序的非简写方法,即 .on() 方法本身。

我们传递给 .on() 的第一个参数是我们要监听的事件的名称。在这里,我们可以使用一种特殊的语法,允许我们对事件进行子分类:

$(() => {
  $('#switcher')
    .on('click.collapse', (e) => {
      if (!$(e.target).is('button')) {
        $(e.currentTarget)
          .children('button')
          .toggleClass('hidden');
      }
    });
  $('#switcher-narrow, #switcher-large')
   .click(() => {
     $('#switcher').off('click.collapse');
   });
}); 

图 3.19

.collapse 后缀对事件处理系统不可见;click 事件由此函数处理,就像我们写了.on('click')一样。然而,添加命名空间意味着我们可以解绑这个处理程序而不影响我们为按钮编写的单独的 click 处理程序。

还有其他使我们的 .off() 调用更加具体的方法,我们马上就会看到。然而,事件命名空间是我们工具库中一个有用的工具。在后面的章节中,我们会看到它在插件的创建中是特别方便的。

重新绑定事件

现在单击窄列或大号字按钮会导致样式切换器折叠功能被禁用。然而,当单击默认按钮时,我们希望行为恢复。为了做到这一点,我们需要在单击默认按钮时重新绑定处理程序。

首先,我们应该给我们的处理程序函数一个名称,这样我们就可以多次使用它而不重复自己:

$(() => {
  const toggleSwitcher = (e) => {
    if (!$(e.target).is('button')) {
      $(e.currentTarget)
        .children('button')
        .toggleClass('hidden');
    }
  };

  $('#switcher')
    .on('click.collapse', toggleSwitcher);
  $('#switcher-narrow, #switcher-large')
    .click((e) => {
      $('#switcher').off('click.collapse');
    });
});

图 3.20

请记住,我们正在将.on()的第二个参数传递给一个函数引用。在引用函数时,重要的是要记住在函数名后省略括号;括号会导致调用函数而不是引用函数。

现在toggleSwitcher()函数已经被引用,我们可以在稍后再次绑定它,而无需重复函数定义:

$(() => {
  const toggleSwitcher = (e) => {
    if (!$(e.target).is('button')) {
      $(e.currentTarget)
        .children('button')
        .toggleClass('hidden');
    }
  };

  $('#switcher').on('click.collapse', toggleSwitcher);
  $('#switcher-narrow, #switcher-large')
    .click(() => {
      $('#switcher').off('click.collapse');
    });
  $('#switcher-default')
    .click(() => {
      $('#switcher').on('click.collapse', toggleSwitcher);
    });
}); 

列表 3.21

现在,切换行为在文档加载时绑定,在点击“Narrow Column”或“Large Print”后取消绑定,并在此后再次点击“Default”时重新绑定。

由于我们已经命名了函数,因此不再需要使用命名空间。.off()方法可以接受一个函数作为第二个参数;在这种情况下,它只取消绑定那个特定的处理程序。但是,我们遇到了另一个问题。请记住,当在 jQuery 中将处理程序绑定到事件时,之前的处理程序仍然有效。在这种情况下,每次点击“Default”时,都会向样式切换器绑定toggleSwitcher处理程序的另一个副本。换句话说,每次额外点击,该函数都会多调用一次,直到用户点击“Narrow”或“Large Print”,这样一次性取消所有toggleSwitcher处理程序。

当绑定了偶数个toggleSwitcher处理程序时,在样式切换器上(而不是在按钮上)点击似乎没有效果。实际上,hidden类被多次切换,最终处于与开始时相同的状态。为了解决这个问题,当用户点击任何按钮时,我们可以取消绑定处理程序,并且只在确保点击的按钮的 ID 为switcher-default后再次绑定:

$(() => {
  const toggleSwitcher = (e) => {
    if (!$(e.target).is('button')) {
      $(e.currentTarget)
        .children('button')
        .toggleClass('hidden');
    }
  };

  $('#switcher')
    .on('click', toggleSwitcher);
  $('#switcher button')
    .click((e) => {
      $('#switcher').off('click', toggleSwitcher);

      if (e.target.id == 'switcher-default') {
        $('#switcher').on('click', toggleSwitcher);
      }
    });
});

列表 3.22

在我们希望在事件触发后立即取消绑定事件处理程序的情况下,也有一个快捷方式可用。这个快捷方式称为.one(),用法如下:

$('#switcher').one('click', toggleSwitcher); 

这会导致切换操作仅发生一次。

模拟用户交互

有时,即使事件不是直接由用户输入触发,执行我们绑定到事件的代码也很方便。例如,假设我们希望我们的样式切换器以折叠状态开始。我们可以通过从样式表中隐藏按钮,或者通过添加我们的hidden类或从$(() => {})处理程序调用.hide()方法来实现这一点。另一种方法是模拟点击样式切换器,以触发我们已经建立的切换机制。

.trigger()方法允许我们做到这一点:

$(() => { 
  $('#switcher').trigger('click'); 
}); 

列表 3.23

当页面加载时,开关器的状态会折叠起来,就好像它已经被点击一样,如下截图所示:

如果我们要隐藏希望禁用 JavaScript 的人看到的内容,这将是实现优雅降级的一个合理方式。尽管,这在如今非常不常见。

.trigger() 方法提供了与 .on() 相同的快捷方法。当这些快捷方式没有参数时,行为是触发操作而不是绑定它:

$(() => {
  $('#switcher').click(); 
}); 

例 3.24

对键盘事件的反应

另一个例子,我们可以向我们的样式切换器添加键盘快捷方式。当用户键入其中一个显示样式的第一个字母时,我们将使页面表现得就像相应的按钮被点击一样。要实现此功能,我们需要探索键盘事件,它们与鼠标事件的行为有些不同。

有两种类型的键盘事件:直接对键盘做出反应的事件(keyupkeydown)以及对文本输入做出反应的事件(keypress)。单个字符输入事件可能对应多个键,例如,当Shift键与X键结合创建大写字母X时。虽然具体的实现细节因浏览器而异(不出所料),但一个安全的经验法则是:如果你想知道用户按下了什么键,你应该观察 keyupkeydown 事件;如果你想知道最终在屏幕上呈现了什么字符,你应该观察 keypress 事件。对于这个功能,我们只想知道用户何时按下了DNL 键,所以我们将使用 keyup

接下来,我们需要确定哪个元素应该监听事件。这对于鼠标事件来说不太明显,因为我们有一个可见的鼠标光标来告诉我们事件的目标。相反,键盘事件的目标是当前具有键盘焦点的元素。焦点元素可以通过多种方式进行更改,包括使用鼠标点击和按下Tab键。而且,并非每个元素都可以获得焦点;只有具有默认键盘驱动行为的项,如表单字段、链接和具有 .tabIndex 属性的元素,才是候选项。

在这种情况下,我们并不真的关心哪个元素获得了焦点;我们希望我们的切换器在用户按下这些键时起作用。事件冒泡将再次派上用场,因为我们可以将我们的 keyup 事件绑定到 document 元素,并确保最终任何键事件都会冒泡到我们这里来。

最后,当我们的 keyup 处理程序被触发时,我们需要知道按下了哪个键。我们可以检查 event 对象来获取这个信息。事件的 .which 属性包含了按下的键的标识符,对于字母键,这个标识符是大写字母的 ASCII 值。有了这个信息,我们现在可以创建一个字母及其相应按钮的对象文本。当用户按下一个键时,我们将查看它的标识符是否在映射中,如果是,就触发点击:

$(() => {
  const triggers = {
    D: 'default',
    N: 'narrow',
    L: 'large'
  };

  $(document)
    .keyup((e) => {
      const key = String.fromCharCode(e.which);

      if (key in triggers) {
        $(`#switcher-${triggers[key]}`).click();
      }
    });
});

例 3.25

连续按下这三个键现在模拟了对按钮的鼠标点击操作——前提是键盘事件没有被诸如 Firefox 在我开始输入时搜索文本这样的功能所中断。

作为使用.trigger()模拟此点击的替代方案,让我们探讨如何将代码因子化为一个函数,以便多个处理程序可以调用它——在这种情况下,clickkeyup处理程序都可以调用它。虽然在本例中并不必要,但这种技术可以有助于消除代码的冗余:

$(() => {
  // Enable hover effect on the style switcher
  const toggleHover = (e) => {
    $(e.target).toggleClass('hover');
  };

  $('#switcher').hover(toggleHover, toggleHover);

  // Allow the style switcher to expand and collapse.
  const toggleSwitcher = (e) => {
    if (!$(e.target).is('button')) {
      $(e.currentTarget)
        .children('button')
        .toggleClass('hidden');
    }
  };

  $('#switcher')
    .on('click', toggleSwitcher)
    // Simulate a click so we start in a collaped state.
    .click();

  // The setBodyClass() function changes the page style.
  // The style switcher state is also updated.
  const setBodyClass = (className) => {
    $('body')
      .removeClass()
      .addClass(className);

    $('#switcher button').removeClass('selected');
    $(`#switcher-${className}`).addClass('selected');
    $('#switcher').off('click', toggleSwitcher);

    if (className == 'default') {
      $('#switcher').on('click', toggleSwitcher);
    }
  };

  // Begin with the switcher-default button "selected"
  $('#switcher-default').addClass('selected');

  // Map key codes to their corresponding buttons to click
  const triggers = {
    D: 'default',
    N: 'narrow',
    L: 'large'
  };

  // Call setBodyClass() when a button is clicked.
  $('#switcher')
    .click((e) => {
      if ($(e.target).is('button')) {
        setBodyClass(e.target.id.split('-')[1]);
      }
    });

  // Call setBodyClass() when a key is pressed.
  $(document)
    .keyup((e) => {
      const key = String.fromCharCode(e.which);

      if (key in triggers) {
        setBodyClass(triggers[key]);
      }
    });
}); 

列表 3.26

最终修订版将本章所有先前的代码示例整合在一起。我们将整个代码块移入一个单独的$(() => {})处理程序,并使我们的代码不那么冗余。

摘要

本章讨论的功能使我们能够对各种用户驱动和浏览器启动的事件作出反应。我们已经学会了如何在页面加载时安全执行操作,如何处理鼠标事件(如单击链接或悬停在按钮上),以及如何解释按键。

此外,我们已经深入研究了事件系统的一些内部工作原理,并可以利用这些知识进行事件委托和更改事件的默认行为。我们甚至可以模拟用户发起事件的效果。

我们可以利用这些功能来构建交互式页面。在下一章中,我们将学习如何在这些交互过程中为用户提供视觉反馈。

进一步阅读

事件处理的主题将在第十章“高级事件”中更详细地探讨。jQuery 的事件方法的完整列表可在本书的附录 C 中找到,或者在官方 jQuery 文档中找到:api.jquery.com/

练习

挑战练习可能需要使用官方 jQuery 文档:api.jquery.com/

  1. 当点击查尔斯·狄更斯时,应用selected样式。

  2. 双击章标题(<h3 class="chapter-title">)时,切换章节文本的可见性。

  3. 当用户按下右箭头键时,循环到下一个body类。右箭头键的键码为39

  4. 挑战:使用console.log()函数记录鼠标在任何段落上移动时的坐标。(注意:console.log()通过 Firefox 的 Firebug 扩展、Safari 的 Web Inspector 或 Chrome 或 Internet Explorer 的开发者工具显示其结果)。

  5. 挑战:使用.mousedown().mouseup()来跟踪页面上任何位置的鼠标事件。如果鼠标按钮在按下的地方上方释放,将hidden类添加到所有段落。如果在按下的地方下方释放,将从所有段落中移除hidden类。

第四章:样式和动画

如果说行动胜于言辞,那么在 JavaScript 世界中,效果使行动更加响亮。通过 jQuery,我们可以轻松地为我们的行动增添影响力,通过一组简单的视觉效果甚至制作我们自己复杂的动画。

jQuery 提供的效果为页面增添了简单的视觉华丽效果,赋予了现代感和动感。然而,除了仅仅是装饰之外,它们还可以提供重要的可用性增强,帮助用户在页面上发生某些事件时进行定位(尤其在 Ajax 应用程序中常见)。

在本章中,我们将涵盖:

  • 动态改变元素的样式

  • 使用各种内置效果隐藏和显示元素

  • 创建元素的自定义动画

  • 将效果按顺序连续发生

修改内联属性的 CSS

在我们深入研究 jQuery 效果之前,先来简要了解一下 CSS 是如何使用的。在之前的章节中,我们通过在单独的样式表中为类定义样式,然后使用 jQuery 添加或删除这些类来修改文档的外观。通常,这是将 CSS 注入 HTML 的首选过程,因为它尊重样式表在处理页面呈现方面的作用。然而,有时我们可能需要应用还没有或者不能轻松定义在样式表中的样式。幸运的是,jQuery 为这种情况提供了.css()方法。

这个方法既作为获取器又作为设置器。要获取单个样式属性的值,我们只需将属性名称作为字符串传递,然后返回一个字符串。要获取多个样式属性的值,我们可以将属性名称作为字符串数组传递,然后返回属性值对的对象。多词属性名称,例如backgroundColor,可以通过 jQuery 在连字符化的 CSS 表示法(background-color)或驼峰式的 DOM 表示法(backgroundColor)中解释:

// Get a single property's value 
.css('property') 
// "value" 

// Get multiple properties' values 
.css(['property1', 'property-2']) 
// {"property1": "value1", "property-2": "value2"} 

对于设置样式属性,.css()方法有两种形式。一种形式接受单个样式属性及其值,另一种形式接受属性值对的对象:

// Single property and its value 
.css('property', 'value') 

// Object of property-value pairs 
.css({ 
  property1: 'value1', 
  'property-2': 'value2' 
}) 

这些简单的键值集合,称为对象字面量,是直接在代码中创建的真正的 JavaScript 对象。

对象字面量表示法

在属性值中,字符串通常像往常一样用引号括起来,但是其他数据类型如数字则不需要。由于属性名称是字符串,因此它们通常会被包含在引号中。然而,如果属性名称是有效的 JavaScript 标识符,比如在驼峰式的 DOM 表示法中书写时,属性名称就不需要引号。

我们使用.css()方法的方式与使用.addClass()方法的方式相同;我们将其应用于一个指向 DOM 元素集合的 jQuery 对象。为了演示这一点,我们将玩弄一个类似于第三章中的样式切换器,处理事件

<div id="switcher"> 
  <div class="label">Text Size</div> 
  <button id="switcher-default">Default</button> 
  <button id="switcher-large">Bigger</button> 
  <button id="switcher-small">Smaller</button> 
</div> 
<div class="speech"> 
  <p>Fourscore and seven years ago our fathers brought forth 
       on this continent a new nation, conceived in liberty,  
       and dedicated to the proposition that all men are created  
       equal.</p> 
  ... 
</div> 

获取示例代码

您可以从 GitHub 存储库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

通过链接到具有几个基本样式规则的样式表,页面将最初呈现如下:

完成我们的代码后,单击“Bigger”和“Smaller”按钮将增加或减小 <div class="speech"> 的文本大小,而单击“Default”按钮将重置 <div class="speech"> 的原始文本大小。

设置计算的样式属性值

如果我们想要的仅仅是将字体大小一次性更改为预定值,我们仍然可以使用 .addClass() 方法。但是,现在假设我们想要每次单击相应按钮时文本都继续递增或递减。虽然可能可以为每次单击定义一个单独的类并对其进行迭代,但更简单的方法是每次通过获取当前大小并增加固定因子(例如,40%)来计算新的文本大小。

我们的代码将以 $(() => {})$('#switcher-large').click() 事件处理程序开头:

$(() => {
  $('#switcher-large')
    .click((e) => { 

    }); 
}); 

列表 4.1

接下来,可以使用 .css() 方法轻松发现字体大小:$('div.speech').css('fontSize')。然而,返回的值是一个字符串,包含数值字体大小值和该值的单位(px)。我们需要去掉单位标签,以便使用数值进行计算。另外,当我们计划多次使用一个 jQuery 对象时,通常最好通过将生成的 jQuery 对象存储在常量中来缓存选择器:

$(() => { 
  const $speech = $('div.speech'); 
  $('#switcher-large')
    .click(() => {
      const num = parseFloat($speech.css('fontSize')); 
    }); 
}); 

列表 4.2

$(() => {}) 内的第一行创建一个包含指向 <div class="speech"> 的 jQuery 对象的常量。注意名称中的美元符号($),$speech。由于美元符号是 JavaScript 标识符中的合法字符,我们可以用它来提醒常量是一个 jQuery 对象。与其他编程语言(如 PHP)不同,美元符号在 JavaScript 中没有特殊意义。

使用常量(const)而不是变量(var)有充分的理由。常量是在 JavaScript 的 ES2015 版本中引入的,它们可以帮助减少某些类型的错误。以我们的 $speech 常量为例。它除了 <div class="speech"> 之外,会持有其他值吗?不会。由于我们声明了这是一个常量,试图给 $speech 分配另一个值会导致错误。这些错误很容易修复。如果 $speech 被声明为一个变量,并且我们错误地给它分配了一个新值,那么失败将是微妙且难以诊断的。当然,有时我们需要能够分配新值,在这种情况下,您将使用一个变量。

.click() 处理程序内部,我们使用 parseFloat() 仅获取字体大小属性的数值部分。parseFloat() 函数从左到右查看字符串,直到遇到一个非数字字符为止。数字字符串被转换为浮点数(十进制数)。例如,它会将字符串'12'转换为数字12。此外,它会从字符串中去除非数字的尾随字符,因此'12px'变成了12。如果字符串以非数字字符开头,parseFloat() 将返回 NaN,表示不是一个数字

唯一剩下的就是修改解析出来的数值并根据新值重置字体大小。在我们的示例中,每次点击按钮时,我们将字体大小增加 40%。为此,我们将 num 乘以 1.4,然后通过连接结果和'px'来设置字体大小:

$(() => {
  const $speech = $('div.speech');

  $('#switcher-large')
    .click(() => {
      const num = parseFloat($speech.css('fontSize'));
      $speech.css('fontSize', `${num * 1.4}px`);
    });
}); 

清单 4.3

现在,当用户点击“放大”按钮时,文本变大了。再次点击,文本就变得更大了:

图 4.4

要让“缩小”按钮减小字体大小,我们将使用除法而不是乘法:num / 1.4。更好的是,我们将两者合并成一个单一的.click()处理程序,适用于<div id="switcher">中的所有<button>元素。然后,在找到数值之后,我们可以根据被点击的按钮的 ID 来乘以或除以。清单 4.4 对此进行了说明。

$(() => {
  const sizeMap = {
    'switcher-small': n => n / 1.4,
    'switcher-large': n => n * 1.4
  };

  const $speech = $('div.speech');

  $('#switcher button')
    .click((e) => {
      const num = parseFloat($speech.css('fontSize'));
      $speech.css(
        'fontSize',
        `${sizeMape.target.id}px`
      );
    });
}); 

清单 4.4

e.target.id 值用于确定点击事件的行为。sizeMap 是存储这些行为的地方。这是一个简单的对象,将元素 ID 映射到一个函数。此函数接收当前的 fontSize 值。我们之所以想使用这样的映射,是因为它比编码为 if 语句之类的东西更容易添加或删除行为。例如,假设当前的字体大小是"10px",用户点击了“放大”按钮。那么,模板字符串${sizeMape.target.id}px 将导致"14px"

让字体大小恢复到初始值也是很好的。为了让用户能够这样做,我们可以简单地在 DOM 准备就绪时将字体大小存储在一个变量中。然后,每当点击“默认”按钮时,我们就可以恢复这个值。我们只需要向 sizeMap 添加另一个函数:

$(() => {
  const sizeMap = {
    'switcher-small': n => n / 1.4,
    'switcher-large': n => n * 1.4,
    'switcher-default': () => defaultSize
  };

  const $speech = $('div.speech');
  const defaultSize = parseFloat($speech.css('fontSize'));

  $('#switcher button')
    .click((e) => {
      const num = parseFloat($speech.css('fontSize'));
      $speech.css(
        'fontSize',
        `${sizeMape.target.id}px`
      );
    });
}); 

清单 4.5

注意我们根本不需要更改点击处理程序来适应这种新行为?我们创建了一个名为defaultSize的新常量,它将始终保存原始字体大小。然后,我们只需要为switcher-default ID 添加一个新函数到 sizeMap 中,该函数返回 defaultSize 的值。

使用这样的映射,更容易改变我们的点击处理程序行为,而不必维护 ifswitch 语句。

使用特定于供应商的样式属性

当浏览器供应商引入实验性的样式属性时,通常会在属性名称前加上前缀,直到浏览器的实现与 CSS 规范一致为止。当实现和规范足够稳定时,供应商会去除该前缀,并允许使用标准名称。因此,在样式表中,看到如下一组 CSS 声明是很常见的:

-webkit-property-name: value; 
-moz-property-name: value; 
-ms-property-name: value; 
-o-property-name: value; 
property-name: value; 

如果我们想要在 JavaScript 中应用相同的效果,我们需要测试 DOM 版本的这些变化的存在性:propertyNameWebkitPropertyNamemsPropertyName 等等。然而,使用 jQuery,我们可以简单地应用标准属性名称,例如 .css('propertyName', 'value')。如果在样式对象的属性中找不到该名称,则 jQuery 在幕后循环遍历供应商前缀--WebkitOMozms--并使用找到的第一个作为属性,如果有的话。

隐藏和显示元素

基本的 .hide().show() 方法,没有任何参数,可以被视为 .css('display', 'string') 的智能快捷方式方法,其中 'string' 是适当的显示值。效果如预期,匹配的元素集将立即隐藏或显示,没有动画。

.hide() 方法将匹配元素集的内联样式属性设置为 display: none。这里的巧妙之处在于它记住了显示属性的值--通常是 blockinlineinline-block--在改为 none 之前的值。相反,.show() 方法将匹配元素集的显示属性恢复为在应用 display: none 之前的初始值。

显示属性

要了解更多关于 display 属性以及它的值在网页中的视觉呈现方式的信息,请访问 Mozilla Developer Center developer.mozilla.org/zh-CN/docs/Web/CSS/display,并查看示例 developer.mozilla.org/samples/cssref/display.html

.show().hide() 的这个特性在隐藏已在样式表中被覆盖其默认 display 属性的元素时尤其有帮助。例如,<li> 元素默认具有 display: list-item 属性,但我们可能希望将其更改为 display: inline 以用于水平菜单。幸运的是,对一个隐藏的元素(比如其中一个 <li> 标签)使用 .show() 方法不会简单地将其重置为其默认的 display: list-item,因为这会将 <li> 标签放在自己的一行上。相反,该元素被恢复到其先前的 display: inline 状态,从而保持了水平设计。

我们可以通过在示例 HTML 中的第一个段落后添加一个“阅读更多”链接来设置这两种方法的快速演示:

<div class="speech"> 
  <p>Fourscore and seven years ago our fathers brought forth  
       on this continent a new nation, conceived in liberty,  
       and dedicated to the proposition that all men are  
       created equal. 
  </p> 
  <p>Now we are engaged in a great civil war, testing whether  
       that nation, or any nation so conceived and so dedicated,  
       can long endure. We are met on a great battlefield of  
       that war. We have come to dedicate a portion of that  
       field as a final resting-place for those who here gave  
       their lives that the nation might live. It is altogether  
       fitting and proper that we should do this. But, in a  
       larger sense, we cannot dedicate, we cannot consecrate,  
       we cannot hallow, this ground. 
  </p> 
  <a href="#" class="more">read more</a> 
    ... 
</div> 

当 DOM 就绪时,我们选择一个元素并对其调用 .hide() 方法:

$(() => {
  $('p')
    .eq(1)
    .hide();   
}); 

清单 4.6

.eq()方法类似于第二章选择元素中讨论的:eq()伪类。它返回一个 jQuery 对象,指向提供的从零开始的索引处的单个元素。在这种情况下,该方法选择第二个段落并隐藏它,以便在第一个段落后立即显示“阅读更多”链接:

当用户在第一个段落末尾点击“阅读更多”时,我们调用.show()来显示第二个段落,并调用.hide()来隐藏点击的链接:

$(() => {
  $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();
      $('p')
        .eq(1)
        .show();
      $(e.target)
        .hide();
    });
});

清单 4.7

注意使用.preventDefault()来阻止链接触发其默认操作。现在演讲看起来像这样:

.hide().show()方法快速实用,但并不十分引人注目。为了增添一些风彩,我们可以给它们指定持续时间。

效果和持续时间

当我们在.show().hide()中包含持续时间(有时也称为速度)时,它就会变成动画效果——在指定的时间段内发生。例如,.hide(duration)方法会逐渐减小元素的高度、宽度和不透明度,直到这三者都达到零,此时将应用 CSS 规则display: none.show(duration)方法将增加元素的高度从顶部到底部,宽度从左边到右边,不透明度从 0 到 1,直到其内容完全可见。

加速中

使用任何 jQuery 效果时,我们可以使用两种预设速度之一,'slow''fast'。使用.show('slow')使显示效果在 600 毫秒(0.6 秒)内完成,.show('fast')在 200 毫秒内完成。如果提供了任何其他字符串,jQuery 将使用默认的动画持续时间 400 毫秒。为了更精确地控制,我们可以指定毫秒数,例如.show(850)

让我们在显示亚伯拉罕·林肯的《葛底斯堡演说》第二段时包含一个速度示例:

$(() => {
  $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();
      $('p')
        .eq(1)
        .show('slow');
      $(e.target)
        .hide();
    });
});

清单 4.8

当我们大致捕捉到段落在效果中的中间时,我们看到以下内容:

淡入和淡出

尽管动画化的.show().hide()方法确实很引人注目,但在实践中,它们会比有用的属性更多地进行动画处理。幸运的是,jQuery 提供了另外几种预定义动画,效果更为微妙。例如,要使整个段落逐渐增加不透明度而出现,我们可以使用fadeIn('slow')代替:

$(() => {
  $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();
      $('p')
        .eq(1)
        .fadeIn('slow');
      $(e.target)
        .hide();
    });
});

清单 4.9

现在当我们在效果进行时观察段落时,它看起来像这样:

这里的不同之处在于.fadeIn()效果首先设置段落的尺寸,使内容可以简单地淡入其中。为了逐渐减少不透明度,我们可以使用.fadeOut()

滑动向上和向下

淡入淡出动画对于在文档流之外的项目非常有用。例如,这些是覆盖在页面上的灯箱元素上的典型效果。但是,当一个元素是文档流的一部分时,在其上调用.fadeIn()会导致文档跳转以提供新元素所需的房地产,这并不美观。

在这些情况下,jQuery 的.slideDown().slideUp()方法是正确的选择。这些效果仅对所选元素的高度进行动画处理。要使我们的段落以垂直滑动效果显示,我们可以调用.slideDown('slow')

$(() => {
  $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();
      $('p')
        .eq(1)
        .slideDown('slow');
      $(e.target)
        .hide();
    });
});

列表 4.10

这次当我们检查动画中点的段落时,我们看到以下内容:

要撤销这种效果,我们将调用.slideUp()方法。

切换可见性

有时我们需要切换元素的可见性,而不是像之前的示例中一样仅显示它们一次。这种切换可以通过首先检查匹配元素的可见性,然后调用适当的方法来实现。再次使用淡入淡出效果,我们可以修改示例脚本如下:

$(() => {
  const $firstPara = $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();

      if ($firstPara.is(':hidden')) {
        $firstPara.fadeIn('slow');
        $(e.target).text('read less');
      } else {
        $firstPara.fadeOut('slow');
        $(e.target).text('read more');
      }
    });
}); 

列表 4.11

正如我们在本章前面所做的那样,我们在这里缓存了我们的选择器,以避免重复的 DOM 遍历。还要注意,我们不再隐藏点击的链接;相反,我们正在更改其文本。

要检查元素包含的文本并更改该文本,我们使用.text()方法。我们将在第五章,操作 DOM中更深入地探讨此方法。

使用if-else语句是切换元素可见性的完全合理的方法。但是,通过 jQuery 的复合效果方法,我们可以从代码中删除一些条件逻辑。jQuery 提供了复合方法.fadeToggle().slideToggle(),它们使用相应的效果显示或隐藏元素。当我们使用.slideToggle()方法时,脚本看起来是这样的:

$(() => {
  const $firstPara = $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();

      $firstPara.slideToggle('slow');
      $(e.target)
        .text(
          $(e.target).text() === 'read more' ?
            'read less' : 'read more'
        );
    });
}); 

列表 4.12

三元表达式$(e.target).text() === 'read more' ?)检查链接的文本而不是第二段落的可见性,因为我们只是用它来更改文本。当我们需要基于某些条件获取值时,我们可以使用三元表达式作为完整的if语句的较短替代方法。把三元表达式想象成调用一个函数,根据提供的参数返回不同的值。

创建自定义动画

除了预先构建的效果方法外,jQuery 还提供了一个强大的.animate()方法,允许我们使用精细的控制创建自己的自定义动画。.animate()方法有两种形式。第一种最多接受四个参数:

  • 一个包含样式属性和值的对象,与本章前面讨论的.css()参数类似

  • 一个可选的持续时间,可以是预设字符串之一,也可以是毫秒数

  • 可选的缓动类型,这是一个我们现在不会使用的选项,但我们将在第十一章 高级效果中讨论它。

  • 可选的回调函数,稍后在本章中讨论

总之,这四个参数看起来像这样:

.animate(
  { property1: 'value1', property2: 'value2'},  
  duration,
  easing,
  () => { 
    console.log('The animation is finished.'); 
  } 
); 

第二种形式接受两个参数:一个属性对象和一个选项对象:

.animate({properties}, {options}) 

在这种形式中,第二个参数将第一种形式的第二到第四个参数包装到另一个对象中,并将一些更高级的选项加入其中。以下是传递实际参数时的第二种形式:

.animate(
  { 
    property1: 'value1',  
    property2: 'value2' 
  },
  { 
    duration: 'value',  
    easing: 'value', 
    specialEasing: { 
      property1: 'easing1', 
      property2: 'easing2' 
    }, 
    complete: () => { 
      console.log('The animation is finished.'); 
    }, 
    queue: true, 
    step: callback 
  }
); 

现在,我们将使用.animate()方法的第一种形式,但在本章讨论排队效果时,我们将返回到第二种形式。

手动构建效果

我们已经看到了几种用于显示和隐藏元素的预包装效果。在讨论.animate()方法之前,通过调用.slideToggle()使用这个较低级别的接口实现相同的结果将是有用的。用我们的自定义动画替换前面示例中的.slideToggle()行非常简单:

$(() => {
  const $firstPara = $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();

      $firstPara.animate({ height: 'toggle' }, 'slow');
      $(e.target)
        .text(
          $(e.target).text() === 'read more' ?
            'read less' : 'read more'
        );
    }); 
}); 

列表 4.13

这不是.slideToggle()的完美替代品;实际的实现还会动画化元素的边距和填充。

如示例所示,.animate()方法提供了用于 CSS 属性的方便的简写值,如'show''hide''toggle',以简化当我们想要模拟预包装效果方法如.slideToggle()的行为时的过程。

同时动画多个属性

使用.animate()方法,我们可以同时修改任意组合的属性。例如,要在切换第二段落时创建一个同时滑动和淡出的效果,我们只需将opacity添加到传递给.animate()的属性中即可:

$(() => {
  const $firstPara = $('p')
    .eq(1)
    .hide();

  $('a.more')
    .click((e) => {
      e.preventDefault();

      $firstPara.animate({
        opacity: 'toggle',
        height: 'toggle'
      }, 'slow');
      $(e.target)
        .text(
          $(e.target).text() === 'read more' ?
            'read less' : 'read more'
        );
    }); 
}); 

列表 4.14

另外,我们不仅可以使用简写效果方法中使用的样式属性,还可以使用数值 CSS 属性,例如lefttopfontSizemarginpaddingborderWidth。在列表 4.5中,我们改变了段落的文本大小。我们可以通过简单地将.animate()方法替换为.css()方法来动画化文本大小的增加或减少:

$(() => {
  const sizeMap = {
    'switcher-small': n => n / 1.4,
    'switcher-large': n => n * 1.4,
    'switcher-default': () => defaultSize
  };

  const $speech = $('div.speech');
  const defaultSize = parseFloat($speech.css('fontSize'));

  $('#switcher button')
    .click((e) => {
      const num = parseFloat($speech.css('fontSize'));
      $speech.animate({
        fontSize: `${sizeMape.target.id}px`
      });
    });
}); 

列表 4.15

额外的动画属性允许我们创建更复杂的效果。例如,我们可以将一个项目从页面的左侧移动到右侧,同时将其高度增加 20 像素,并将其边框宽度更改为 5 像素。我们将用<div id="switcher">框来说明这一复杂的属性动画集。在我们对其进行动画化之前,它是这样的:

对于具有可伸缩宽度布局,我们需要计算框在与页面右侧对齐之前需要移动的距离。假设段落的宽度是 100%,我们可以从段落的宽度中减去“文本大小”框的宽度。我们可以使用 jQuery.outerWidth() 方法来计算这些宽度,包括填充和边框。我们将使用此方法来计算 switcher 的新 left 属性。为了本例子的目的,我们将通过点击按钮上方的“文本大小”标签来触发动画。下面是代码应该的样子:

$(() => {
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher.animate({
        borderWidth: '5px',
        left: paraWidth - switcherWidth,
        height: '+=20px'
      }, 'slow');
    });
}); 

列表 4.16

有必要详细检查这些动画属性。borderWidth 属性很直接,因为我们正在指定一个带单位的常量值,就像在样式表中一样。left 属性是一个计算出的数字值。在这些属性上,单位后缀是可选的;因为我们在这里省略了它,所以假定为 px。最后,height 属性使用了我们之前未见过的语法。在属性值上的 += 前缀表示相对值。所以,不是将高度动画变为 20 像素,而是将高度动画变为当前高度的 20 像素更高。由于涉及到特殊字符,相对值必须指定为字符串。

尽管此代码成功增加了 <div> 标签的高度并扩大了其边框,但是目前,left 位置似乎未更改:

我们仍然需要在 CSS 中启用更改此框的位置。

使用 CSS 进行定位

在使用 .animate() 时,重要的是要记住 CSS 对我们希望更改的元素施加的限制。例如,调整 left 属性对匹配元素没有影响,除非这些元素的 CSS 位置设置为 relativeabsolute。所有块级元素的默认 CSS 位置都是 static,这准确描述了如果我们在先更改它们的 position 值之前尝试移动它们,这些元素将保持不变的方式。

有关绝对和相对定位的更多信息,请参阅 CSS 技巧:css-tricks.com/almanac/properties/p/position/

在我们的样式表中,我们可以将 <div id="switcher"> 设置为相对定位:

#switcher { 
  position: relative; 
} 

不过,让我们通过在需要时通过 JavaScript 更改此属性来练习我们的 jQuery 技能:

$(() =>
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher
        .css('position', 'relative')
        .animate({
          borderWidth: '5px',
          left: paraWidth - switcherWidth,
          height: '+=20px'
        }, 'slow');
    });
}); 

列表 4.17

考虑了 CSS 后,在动画完成后点击“文本大小”后的结果如下:

同时执行与顺序执行的效果

正如我们刚刚发现的那样,.animate() 方法对影响特定一组元素的同时效果非常有用。然而,有时候我们想要排队我们的效果,以便它们一个接一个地发生。

使用单组元素

当对同一组元素应用多个效果时,通过链接这些效果可以轻松实现排队。为了演示这种排队,我们将通过将文本大小框移动到右侧,增加其高度和边框宽度来重新访问列表 4.17。但是,这次,我们只需将每个效果放在自己的.animate()方法中,并将三者链接在一起,便可以依次执行三个效果:

$(() => {
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher
        .css('position', 'relative')
        .animate({ borderWidth: '5px' }, 'slow')
        .animate({ left: paraWidth - switcherWidth }, 'slow')
        .animate({ height: '+=20px' }, 'slow');
    }); 
}); 

列表 4.18

请记住,链式调用允许我们将所有三个.animate()方法保持在同一行上,但是在这里,我们将它们缩进并将每个方法放在自己的一行上以提高可读性。

我们可以通过链接它们来对 jQuery 效果方法中的任何一个进行排队,而不仅仅是.animate()。例如,我们可以按照以下顺序对<div id="switcher">上的效果进行排队:

  1. 使用.fadeTo()将其不透明度淡化为 0.5。

  2. 使用.animate()将其移到右侧。

  3. 使用.fadeTo()将其淡回完全不透明。

  4. 使用.slideUp()将其隐藏。

  5. 使用.slideDown()再次显示它。

我们需要做的就是按照代码中相同的顺序链接效果:

$(() => {
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher
        .css('position', 'relative')
        .fadeTo('fast', 0.5)
        .animate({ left: paraWidth - switcherWidth }, 'slow')
        .fadeTo('slow', 1.0)
        .slideUp('slow')
        .slideDown('slow');
    }); 
}); 

列表 4.19

绕过队列

但是,如果我们想要在淡入到半透明度的同时将<div>标签移到右侧怎么办?如果两个动画的速度相同,我们可以简单地将它们合并为一个.animate()方法。但是,在这个例子中,淡出使用了'快'速度,而移到右侧则使用了'慢'速度。这就是第二种形式的.animate()方法派上用场的地方:

$(() => {
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher
        .css('position', 'relative')
        .fadeTo('fast', 0.5)
        .animate(
          { left: paraWidth - switcherWidth },
          { duration: 'slow', queue: false }
        )
        .fadeTo('slow', 1.0)
        .slideUp('slow')
        .slideDown('slow');
    }); 
}); 

列表 4.20

第二个参数,一个选项对象,提供了queue选项,当设置为false时,使动画与前一个动画同时开始。如果你考虑一下,这是有道理的,因为任何在队列中的东西都必须等待已经在队列中的东西。

手动排队效果

关于单一元素队列效应的最后观察是,排队不会自动应用于其他非效果方法,例如.css()。所以,假设我们想在.slideUp()方法之后但在slideDown()方法之前将<div id="switcher">的背景颜色更改为红色。

我们可以尝试这样做:

$(() => {
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher
        .css('position', 'relative')
        .fadeTo('fast', 0.5)
        .animate(
          { left: paraWidth - switcherWidth },
          { duration: 'slow', queue: false }
        )
        .fadeTo('slow', 1.0)
        .slideUp('slow')
        .css('backgroundColor', '#f00')
        .slideDown('slow');
    }); 
}); 

列表 4.21

然而,尽管改变背景颜色的代码被放置在链中的正确位置,但它会立即在点击时发生。

我们可以使用名为.queue()的方法将非效果方法添加到队列中。以下是在我们的示例中的样子:

$(() => {
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher
        .css('position', 'relative')
        .fadeTo('fast', 0.5)
        .animate(
          { left: paraWidth - switcherWidth },
          { duration: 'slow', queue: false }
        )
        .fadeTo('slow', 1.0)
        .slideUp('slow')
        .queue((next) => {
          $switcher.css('backgroundColor', '#f00');
          next();
        })
        .slideDown('slow');
    }); 
}); 

列表 4.22

当即将执行一个回调函数时,.queue()方法将该函数添加到要对匹配元素执行的效果队列中。在函数内部,我们将背景颜色设置为红色,然后调用next(),这是一个作为参数传递给我们的回调函数的函数。包含这个next()函数调用允许动画队列从断点恢复,并用后续的.slideDown('slow')行完成链条。如果我们没有调用next(),动画将停止。

有关.queue()的更多信息和示例,请访问api.jquery.com/category/effects/

当我们研究对多组元素进行效果处理时,我们将发现另一种在非效果方法中排队的方法。

处理多组元素

与单一元素不同,当我们对不同的元素集应用效果时,它们几乎同时发生。为了看到这些同时发生的效果,我们将把一个段落向下滑动,同时将另一个段落向上滑动。我们将处理我们示例文档中的第三段和第四段:

<p>Fourscore and seven years ago our fathers brought forth 
   on this continent a new nation, conceived in liberty, 
   and dedicated to the proposition that all men are 
   created equal.</p> 
<p>Now we are engaged in a great civil war, testing whether 
   that nation, or any nation so conceived and so 
   dedicated, can long endure. We are met on a great 
   battlefield of that war. We have come to dedicate a 
   portion of that field as a final resting-place for those 
   who here gave their lives that the nation might live. It 
   is altogether fitting and proper that we should do this. 
   But, in a larger sense, we cannot dedicate, we cannot 
   consecrate, we cannot hallow, this ground.</p> 
<a href="#" class="more">read more</a> 
<p>The brave men, living and dead, who struggled here have 
   consecrated it, far above our poor power to add or 
   detract. The world will little note, nor long remember, 
   what we say here, but it can never forget what they did 
   here. It is for us the living, rather, to be dedicated 
   here to the unfinished work which they who fought here 
   have thus far so nobly advanced.</p> 
<p>It is rather for us to be here dedicated to the great 
   task remaining before us&mdash;that from these honored 
   dead we take increased devotion to that cause for which 
   they gave the last full measure of devotion&mdash;that 
   we here highly resolve that these dead shall not have 
   died in vain&mdash;that this nation, under God, shall 
   have a new birth of freedom and that government of the 
   people, by the people, for the people, shall not perish 
   from the earth.</p> 

为了帮助我们看到效果的发生过程,我们将给第三段添加 1 像素的边框,将第四段添加灰色背景。此外,在 DOM 准备就绪时,我们将隐藏第四段:

$(() => {
  $('p')
    .eq(2)
    .css('border', '1px solid #333');
  $('p')
    .eq(3)
    .css('backgroundColor', '#ccc')
    .hide(); 
}); 

列表 4.23

我们的示例文档现在显示了开头段落,然后是阅读更多链接和有边框的段落:

最后,我们将在第三段应用一个click处理程序,这样当单击它时,第三段将向上滑动(最终滑出视野),而第四段将向下滑动(并进入视野):

$(() => { 
  $('p')
    .eq(2)
    .css('border', '1px solid #333')
    .click((e) => {
      $(e.target)
        .slideUp('slow')
        .next()
        .slideDown('slow');
    });
  $('p')
    .eq(3)
    .css('backgroundColor', '#ccc')
    .hide();
}); 

列表 4.24

在滑动中截取这两个效果的屏幕截图证实它们的确是同时发生的:

第三段开始是可见的,正在向上滑动,与此同时第四段,开始是隐藏的,正在向下滑动。

使用回调函数排队

为了允许在不同元素上排队效果,jQuery 为每个效果方法提供了一个回调函数。正如我们在事件处理程序和.queue()方法中所看到的,回调函数只是作为方法参数传递的函数。至于效果,它们出现在方法的最后一个参数中。

如果我们使用一个回调将这两个滑动效果排队,我们可以让第四段在第三段之前滑下来。让我们先尝试将.slideUp()调用移到.slideDown()方法的完成回调中:

$(() => { 
  $('p')
    .eq(2)
    .css('border', '1px solid #333')
    .click((e) => {
      $(e.target)
        .next()
        .slideDown('slow', () => {
          $(e.target).slideUp('slow');
        });
    });
  $('p')
    .eq(3)
    .css('backgroundColor', '#ccc')
    .hide();
}); 

列表 4.25

如果我们决定在click()回调函数和slideDown()回调函数中都使用$(this),事情将不会按预期进行。因为this是有上下文的。相反,我们可以完全避免它,并引用$(e.target)来获取我们需要的<p>元素。

这一次,在效果进行一半的快照中,第三段和第四段都是可见的;第四段已经滑动下来,第三段即将开始滑动上去:

现在我们已经讨论了回调函数,我们可以返回到清单 4.22中的代码,其中我们在一系列效果的最后排队更改了背景颜色。与当时所做的一样,我们可以简单地使用回调函数,而不是使用.queue()方法:

$(() => {
  $('div.label')
    .click((e) => {
      const $switcher = $(e.target).parent();
      const paraWidth = $('div.speech p').outerWidth();
      const switcherWidth = $switcher.outerWidth();

      $switcher
        .css('position', 'relative')
        .fadeTo('fast', 0.5)
        .animate(
          { left: paraWidth - switcherWidth },
          { duration: 'slow', queue: false }
        )
        .fadeTo('slow', 1.0)
        .slideUp('slow', () => {
          $switcher.css('backgroundColor', '#f00');
        })
        .slideDown('slow');
    });
}); 

清单 4.26

再次说明,在<div id="switcher">滑动上升之后和滑动回落之前,背景色会变为红色。请注意,当使用效果的完成回调而不是.queue()时,我们不需要担心在回调中调用next()

简而言之

考虑到应用效果时的各种变化,记住效果是同时还是顺序发生可能变得困难。简要的大纲可能会有所帮助。

单一元素集上的效果是:

  • 将多个属性同时应用于单个.animate()方法时

  • 在方法链中应用时排队,除非将 queue 选项设置为 false

多个元素集上的效果是:

  • 默认情况下同时进行

  • 在另一个效果的回调中应用时或者在.queue()方法的回调中应用时排队

摘要

使用本章中探讨的效果方法,我们现在应该能够从 JavaScript 修改内联样式属性,将预包装的 jQuery 效果应用于元素,并创建我们自己的自定义动画。特别是,您学会了如何逐步增加和减小文本大小,使用.css().animate() 方法,通过修改多个属性逐渐隐藏和显示页面元素,以及如何以多种方式动画元素(同时或顺序)。

在本书的前四章中,我们的所有示例都涉及到操纵硬编码到页面 HTML 中的元素。在第五章,操作 DOM 中,我们将探讨直接操作 DOM 的方法,包括使用 jQuery 创建新元素并将其插入到我们选择的 DOM 中。

进一步阅读

动画主题将在第十一章,高级效果 中详细探讨。本书附录 B 中提供了完整的效果和样式方法列表,或者在官方 jQuery 文档api.jquery.com/ 中提供。

练习

挑战练习可能需要使用官方 jQuery 文档api.jquery.com/

  1. 修改样式表以最初隐藏页面内容。当页面加载时,逐渐淡入内容。

  2. 只有当鼠标悬停在段落上时,才给每个段落添加黄色背景。

  3. 点击标题(<h2>),同时将其淡出至 25%的不透明度,并将其左边距增加到20px。然后,当此动画完成时,将演讲文本淡出至 50%的不透明度。

  4. 这里有一个挑战给你。通过平滑地移动开关框,对箭头键的按键作出反应,向相应方向移动 20 像素。箭头键的键码分别为:37(左)、38(上)、39(右)和40(下)。

第五章:操纵 DOM

Web 经验是 Web 服务器和 Web 浏览器之间的合作伙伴关系。传统上,生成可供浏览器使用的 HTML 文档一直是服务器的职责。我们在本书中看到的技术略微改变了这种安排,使用 CSS 技术实时改变 HTML 文档的外观。但要真正发挥我们的 JavaScript 实力,你需要学会修改文档本身。

在本章中,我们将涵盖:

  • 使用文档对象模型DOM)提供的接口修改文档

  • 在页面上创建元素和文本

  • 移动或删除元素

  • 通过添加、删除或修改属性和属性,转换文档

操纵属性和属性

在本书的前四章中,我们一直在使用.addClass().removeClass()方法来演示如何在页面上更改元素的外观。尽管我们非正式地讨论了这些方法,提到了操纵class属性,但 jQuery 实际上修改了一个名为className的 DOM 属性。.addClass()方法创建或添加到该属性,而.removeClass()删除或缩短它。再加上.toggleClass()方法,它在添加和删除类名之间切换,我们就有了一种高效而健壮的处理类的方式。这些方法特别有帮助,因为它们在元素上添加类时避免了添加已经存在的类(所以我们不会得到<div class="first first">,例如),并且正确处理应用于单个元素的多个类的情况,例如<div class="first second">

非类属性

我们可能需要不时访问或更改其他几个属性或属性。对于操纵诸如idrelhref之类的属性,jQuery 提供了.attr().removeAttr()方法。这些方法使更改属性变得简单。此外,jQuery 还允许我们一次修改多个属性,类似于我们使用.css()方法在第四章样式和动画中处理多个 CSS 属性的方式。

例如,我们可以轻松地一次设置链接的idreltitle属性。让我们从一些示例 HTML 开始:

<h1 id="f-title">Flatland: A Romance of Many Dimensions</h1> 
<div id="f-author">by Edwin A. Abbott</div> 
<h2>Part 1, Section 3</h2> 
<h3 id="f-subtitle"> 
   Concerning the Inhabitants of Flatland 
</h3> 
<div id="excerpt">an excerpt</div> 
<div class="chapter"> 
  <p class="square">Our Professional Men and Gentlemen are 
    Squares (to which class I myself belong) and Five-Sided  
    Figures or <a  
    href="http://en.wikipedia.org/wiki/Pentagon">Pentagons 
    </a>. 
  </p> 
  <p class="nobility hexagon">Next above these come the  
    Nobility, of whom there are several degrees, beginning at  
    Six-Sided Figures, or <a  
    href="http://en.wikipedia.org/wiki/Hexagon">Hexagons</a>,  
    and from thence rising in the number of their sides till  
    they receive the honourable title of <a  
    href="http://en.wikipedia.org/wiki/Polygon">Polygonal</a>,  
    or many-Sided. Finally when the number of the sides  
    becomes so numerous, and the sides themselves so small,  
    that the figure cannot be distinguished from a <a  
    href="http://en.wikipedia.org/wiki/Circle">circle</a>, he  
    is included in the Circular or Priestly order; and this is  
    the highest class of all. 
  </p> 
  <p><span class="pull-quote">It is a <span class="drop">Law  
    of Nature</span> with us that a male child shall have  
    <strong>one more side</strong> than his father</span>, so  
    that each generation shall rise (as a rule) one step in  
    the scale of development and nobility. Thus the son of a  
    Square is a Pentagon; the son of a Pentagon, a Hexagon;  
    and so on. 
  </p> 
<!-- . . . code continues . . . --> 
</div> 

获取示例代码

您可以从以下 GitHub 存储库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

现在,我们可以迭代<div class="chapter">内的每个链接,并逐个应用属性。如果我们需要为所有链接设置单个属性值,我们可以在我们的$(() => {})处理程序中用一行代码完成:

$(() => {
  $('div.chapter a').attr({ rel: 'external' });
});

列表 5.1

就像.css()方法一样,.attr()也可以接受一对参数,第一个指定属性名,第二个是其新值。不过,更典型的是,我们提供一个键值对的对象,就像在 清单 5.1 中所做的那样。以下语法允许我们轻松地扩展我们的示例以一次修改多个属性:

$(() => {
  $('div.chapter a')
    .attr({
      rel: 'external',
      title: 'Learn more at Wikipedia'
    });
});

清单 5.2

值回调

将一个简单对象传递给.attr()是一个直接的技巧,当我们希望每个匹配的元素具有相同的值时,它就足够了。然而,通常情况下,我们添加或更改的属性必须每次具有不同的值。一个常见的例子是,对于任何给定的文档,如果我们希望我们的 JavaScript 代码表现可预测,那么每个id值必须是唯一的。为每个链接设置唯一的id值,我们可以利用 jQuery 方法的另一个特性,如.css().each()--值回调

值回调只是一个提供给参数的函数,而不是值。然后,对匹配集合中的每个元素调用此函数一次。从函数返回的任何数据都将用作属性的新值。例如,我们可以使用以下技术为每个元素生成不同的id值:

$(() => {
  $('div.chapter a')
    .attr({
      rel: 'external',
      title: 'Learn more at Wikipedia',
      id: index => `wikilink-${index}`
    });
});

清单 5.3

每次调用我们的值回调时,都会传递一个整数,指示迭代计数;在这里,我们正在使用它为第一个链接赋予一个idwikilink-0,第二个wikilink-1,依此类推。

我们正在使用title属性邀请人们在维基百科了解更多有关链接术语的信息。到目前为止,我们使用的 HTML 标签中,所有链接都指向维基百科。但是,为了考虑到其他类型的链接,我们应该使选择器表达式更具体一些:

$(() => {
  $('div.chapter a[href*="wikipedia"]')
    .attr({
      rel: 'external',
      title: 'Learn more at Wikipedia',
      id: index => `wikilink-${index}`
    });
});

清单 5.4

要完成我们对.attr()方法的介绍,我们将增强这些链接的title属性,使其更具体地描述链接目标。再次,值回调是完成工作的正确工具:

$(() => {
  $('div.chapter a[href*="wikipedia"]')
    .attr({
      rel: 'external',
      title: function() {
        return `Learn more about ${$(this).text()} at Wikipedia.`;
      },
      id: index => `wikilink-${index}`
    });
});

清单 5.5

这次我们利用了值回调的上下文。就像事件处理程序一样,关键字this每次调用回调时都指向我们正在操作的 DOM 元素。在这里,我们将元素包装在一个 jQuery 对象中,以便我们可以使用.text()方法(在第四章中介绍的 Styling and Animating)来检索链接的文本内容。这使得每个链接标题与其他链接不同,如下面的屏幕截图所示:

数据属性

HTML5 数据属性允许我们将任意数据值附加到页面元素。然后,我们的 jQuery 代码可以使用这些值,以及修改它们。使用数据属性的原因是我们可以将控制它们的显示和行为的 DOM 属性与特定于我们的应用程序的数据分开。

使用data() jQuery 方法来读取数据值并更改数据值。让我们添加一些新功能,允许用户通过点击来标记段落为已读。我们还需要一个复选框,用于隐藏已标记为已读的段落。我们将使用数据属性来帮助我们记住哪些段落已标记为已读:

$(() => {
  $('#hide-read')
    .change((e) => {
      if ($(e.target).is(':checked')) {
        $('.chapter p')
          .filter((i, p) => $(p).data('read'))
          .hide();
      } else {
        $('.chapter p').show();
      }
    });

  $('.chapter p')
    .click((e) => {
      const $elm = $(e.target);

      $elm
        .css(
          'textDecoration',
          $elm.data('read') ? 'none' : 'line-through'
        )
        .data('read', !$(e.target).data('read'));
    });
});

列表 5.6

当您单击段落时,文本将被标记为已读:

正如您所看到的,点击事件处理程序在段落被点击时改变其视觉外观。但处理程序还做了其他事情--它切换了元素的read数据:data('read', !$(e.target).data('read'))。这让我们能够以一种不干扰我们可能设置的其他 HTML 属性的方式将应用程序特定的数据与元素绑定。

隐藏已读段落复选框的更改处理程序寻找具有此数据的段落。filter((i, p) => $(p).data('read'))调用只会返回具有值为trueread数据属性的段落。我们现在能够根据特定的应用程序数据来过滤元素。以下是隐藏已读段落后页面的外观:

我们将在本书的后期重新讨论一些使用 jQuery 处理数据的高级用法。

DOM 元素属性

正如前面提到的,HTML 属性 和 DOM 属性 之间存在微妙的区别。属性是页面 HTML 源代码中用引号括起来的值,而属性是 JavaScript 访问时的值。我们可以在 Chrome 等开发者工具中轻松观察属性和属性:

Chrome 开发者工具的元素检查器向我们展示了高亮显示的<p>元素具有名为class的属性,其值为square。在右侧面板中,我们可以看到该元素具有一个名为className的对应属性,其值为square。这说明了属性及其等效属性具有不同名称的情况之一。

在大多数情况下,属性和属性在功能上是可以互换的,并且 jQuery 会为我们处理命名不一致性。然而,有时我们确实需要注意两者之间的区别。一些 DOM 属性,如nodeNamenodeTypeselectedIndexchildNodes,没有等效的属性,因此无法通过.attr()访问。此外,数据类型可能不同:例如,checked属性具有字符串值,而checked属性具有布尔值。对于这些布尔属性,最好测试和设置属性而不是属性,以确保跨浏览器行为的一致性。

我们可以使用.prop()方法从 jQuery 获取和设置属性:

// Get the current value of the "checked" property 
const currentlyChecked = $('.my-checkbox').prop('checked'); 

// Set a new value for the "checked" property 
$('.my-checkbox').prop('checked', false); 

.prop()方法具有与.attr()相同的所有功能,例如接受一次设置多个值的对象和接受值回调函数。

表单控件的值

在尝试获取或设置表单控件的值时,属性和属性之间最麻烦的差异也许就是最为令人头疼的。对于文本输入,value属性等同于defaultValue属性,而不是value属性。对于select元素,通常通过元素的selectedIndex属性或其option元素的selected属性来获取值。

由于这些差异,我们应该避免使用.attr()——在select元素的情况下,甚至避免使用.prop()——来获取或设置表单元素的值。相反,我们可以使用 jQuery 为这些场合提供的.val()方法:

// Get the current value of a text input 
const inputValue = $('#my-input').val(); 
// Get the current value of a select list 
const selectValue = $('#my-select').val(); 
//Set the value of a single select list 
$('#my-single-select').val('value3'); 
// Set the value of a multiple select list 
$('#my-multi-select').val(['value1', 'value2']); 

.attr().prop()一样,.val()方法可以接受一个函数作为其设置器参数。借助其多功能的.val()方法,jQuery 再次让 Web 开发变得更加容易。

DOM 树操作

.attr().prop()方法是非常强大的工具,借助它们,我们可以对文档进行有针对性的更改。尽管如此,我们仍然没有看到如何更改文档的整体结构。要真正操作 DOM 树,你需要更多地了解位于jQuery库核心的函数。

$()函数再探讨

从本书的开头,我们一直在使用$()函数来访问文档中的元素。正如我们所见,这个函数充当了一个工厂的角色,产生了指向由 CSS 选择器描述的元素的新的 jQuery 对象。

$()函数的功能远不止于此。它还可以改变页面的内容。只需将一小段 HTML 代码传递给函数,我们就可以创建一个全新的 DOM 结构。

辅助功能提醒

我们应该再次牢记,将某些功能、视觉吸引力或文本信息仅提供给那些能够(并启用了)使用 JavaScript 的 Web 浏览器的人,存在固有的危险。重要信息应该对所有人可访问,而不仅仅是那些使用正确软件的人。

创建新元素

常见于 FAQ 页面的功能之一是在每个问题和答案对之后显示返回顶部链接。可以说这些链接没有任何语义作用,因此它们可以通过 JavaScript 合法地作为页面访问者子集的增强功能。在我们的示例中,我们将在每个段落后面添加一个返回顶部链接,以及返回顶部链接将指向的锚点。首先,我们简单地创建新元素:

$(() => {
  $('<a href="#top">back to top</a>'); 
  $('<a id="top"></a>'); 
}); 

列表 5.7

我们在第一行代码中创建了一个返回顶部链接,在第二行创建了链接的目标锚点。然而,页面上还没有出现返回顶部的链接。

虽然我们编写的两行代码确实创建了元素,但它们还没有将元素添加到页面上。我们需要告诉浏览器这些新元素应该放在哪里。为此,我们可以使用众多 jQuery 插入方法之一。

插入新元素

jQuery 库有许多可用于将元素插入文档的方法。每个方法都规定了新内容与现有内容的关系。例如,我们希望我们的返回顶部链接出现在每个段落后面,因此我们将使用适当命名的 .insertAfter() 方法来实现这一点:

$(() => { 
  $('<a href="#top">back to top</a>')
    .insertAfter('div.chapter p'); 
  $('<a id="top"></a>'); 
}); 

列表 5.8

因此,现在我们实际上已经将链接插入到页面中(并插入到 DOM 中),它们将出现在 <div class="chapter"> 中的每个段落之后:

请注意,新链接出现在自己的一行上,而不是在段落内部。这是因为 .insertAfter() 方法及其对应的 .insertBefore() 方法会在指定元素外部添加内容。

不幸的是,链接还不能使用。我们仍然需要插入带有 id="top" 的锚点。这一次,我们将使用一个在其他元素内部插入元素的方法:

$(() => { 
  $('<a href="#top">back to top</a>')
    .insertAfter('div.chapter p'); 
  $('<a id="top"></a>')
    .prependTo('body'); 
}); 

列表 5.9

这段额外的代码将锚点插入在 <body> 标签的开头;换句话说,位于页面顶部。现在,使用链接的 .insertAfter() 方法和锚点的 .prependTo() 方法,我们有了一个完全功能的返回顶部链接集合。

一旦我们添加了相应的 .appendTo() 方法,我们现在就有了一个完整的选项集,用于在其他元素之前和之后插入新元素:

  • .insertBefore(): 在现有元素之外并且在其前面添加内容

  • .prependTo(): 在现有元素之内并且在其前面添加内容

  • .appendTo(): 在现有元素之内并且在其后面添加内容

  • .insertAfter(): 在现有元素之外并且在其后面添加内容

移动元素

在添加返回顶部链接时,我们创建了新的元素并将它们插入到页面中。还可以将页面上的元素从一个地方移动到另一个地方。这种插入的实际应用是动态放置和格式化脚注。一个脚注已经出现在我们用于此示例的原始 Flatland 文本中,但为了演示目的,我们还将指定文本的另外几部分作为脚注:

<p>How admirable is the Law of Compensation! <span     
   class="footnote">And how perfect a proof of the natural  
   fitness and, I may almost say, the divine origin of the  
   aristocratic constitution of the States of Flatland!</span> 
   By a judicious use of this Law of Nature, the Polygons and  
   Circles are almost always able to stifle sedition in its  
   very cradle, taking advantage of the irrepressible and  
   boundless hopefulness of the human mind.&hellip; 
</p> 

我们的 HTML 文档包含三个脚注;上一个段落包含一个示例。脚注文本位于段落文本内部,使用 <span class="footnote"></span> 进行分隔。通过以这种方式标记 HTML 文档,我们可以保留脚注的上下文。样式表中应用的 CSS 规则使脚注变为斜体,因此受影响的段落最初看起来像下面这样:

现在,我们需要抓取脚注并将它们移动到文档底部。具体来说,我们将它们插入在<div class="chapter"><div id="footer">之间。

请记住,即使在隐式迭代的情况下,处理元素的顺序也是精确定义的,从 DOM 树的顶部开始并向下工作。由于在页面上保持脚注的正确顺序很重要,我们应该使用.insertBefore('#footer')。这将使每个脚注直接放在<div id="footer">元素之前,以便第一个脚注放在<div class="chapter"><div id="footer">之间,第二个脚注放在第一个脚注和<div id="footer">之间,依此类推。另一方面,使用.insertAfter('div.chapter')会导致脚注以相反的顺序出现。

到目前为止,我们的代码看起来像下面这样:

$(() => { 
  $('span.footnote').insertBefore('#footer'); 
}); 

图 5.10

脚注位于<span>标签中,默认情况下显示为内联,一个紧挨着另一个,没有分隔。但是,我们在 CSS 中已经预料到了这一点,在span.footnote元素处于<div class="chapter">之外时,给予了display值为block。因此,脚注现在开始成形:

现在,脚注已经位于正确的位置,但是仍然有很多工作可以做。一个更健壮的脚注解决方案应该执行以下操作:

  1. 对每个脚注编号。

  2. 使用脚注的编号标记从文本中提取每个脚注的位置。

  3. 从文本位置创建到其匹配脚注的链接,并从脚注返回到从文本中提取每个脚注的位置,使用脚注的编号。

包装元素

为了给脚注编号,我们可以在标记中显式添加数字,但是在这里我们可以利用标准的有序列表元素,它会为我们自动编号。为此,我们需要创建一个包含所有脚注的<ol>元素和一个单独包含每个脚注的<li>元素。为了实现这一点,我们将使用包装方法

在将元素包装在另一个元素中时,我们需要明确我们是想让每个元素都包装在自己的容器中,还是所有元素都包装在一个单一的容器中。对于我们的脚注编号,我们需要两种类型的包装器:

$(() => {
  $('span.footnote') 
    .insertBefore('#footer') 
    .wrapAll('<ol id="notes"></ol>') 
    .wrap('<li></li>'); 
}); 

图 5.11

一旦我们在页脚之前插入了脚注,我们就使用.wrapAll()将整个集合包装在一个单独的<ol>元素内。然后,我们继续使用.wrap()将每个单独的脚注包装在其自己的<li>元素内。我们可以看到这样创建了正确编号的脚注:

现在,我们已经准备好标记并编号我们提取脚注的位置。为了以简单直接的方式做到这一点,我们需要重写我们现有的代码,使其不依赖于隐式迭代。

显式迭代

.each()方法充当显式迭代器,与最近添加到 JavaScript 语言中的forEach数组迭代器非常相似。当我们想要对匹配的每个元素使用的代码过于复杂时,可以使用.each()方法。它接受一个回调函数,该函数将对匹配集合中的每个元素调用一次。

$(() => { 
  const $notes = $('<ol id="notes"></ol>')
    .insertBefore('#footer');

  $('span.footnote')
    .each((i, span) => {
      $(span)
        .appendTo($notes)
        .wrap('<li></li>');
    });
}); 

清单 5.12

我们这里的更改动机很快就会变得清晰。首先,我们需要了解传递给我们的.each()回调的信息。

清单 5.12中,我们使用span参数创建一个指向单个脚注<span>的 jQuery 对象,然后将该元素追加到脚注 <ol> 中,最后将脚注包装在一个 <li> 元素中。

为了标记从中提取脚注的文本位置,我们可以利用.each()回调的参数。该参数提供了迭代计数,从0开始,并在每次调用回调时递增。因此,该计数器始终比脚注的数量少 1。在生成文本中的适当标签时,我们将考虑到这一事实:

$(() => { 
  const $notes = $('<ol id="notes"></ol>')
    .insertBefore('#footer');

  $('span.footnote')
    .each((i, span) => {
      $(`<sup>${i + 1}</sup>`)
        .insertBefore(span);
      $(span)
        .appendTo($notes)
        .wrap('<li></li>');
    });
}); 

清单 5.13

现在,在每个脚注被从文本中取出并放置在页面底部之前,我们创建一个包含脚注编号的新 <sup> 元素,并将其插入到文本中。这里的操作顺序很重要;我们需要确保标记被插入到移动脚注之前,否则我们将丢失其初始位置的追踪。

再次查看我们的页面,现在我们可以看到脚注标记出现在原来的内联脚注位置上:

使用反向插入方法

清单 5.13中,我们在一个元素之前插入内容,然后将该元素追加到文档的另一个位置。通常,在 jQuery 中处理元素时,我们可以使用链式操作来简洁高效地执行多个操作。但是在这里,我们无法做到这一点,因为this.insertBefore()目标,同时也是.appendTo()主语反向插入方法将帮助我们克服这个限制。

每个插入方法,如.insertBefore().appendTo(),都有一个对应的反向方法。反向方法执行的任务与标准方法完全相同,但主语和目标被颠倒了。例如:

$('<p>Hello</p>').appendTo('#container'); 

与下面相同:

$('#container').append('<p>Hello</p>'); 

使用.before(),即.insertBefore()的反向形式,现在我们可以重构我们的代码以利用链式操作:

$(() => {
  const $notes = $('<ol id="notes"></ol>')
    .insertBefore('#footer');

  $('span.footnote')
    .each((i, span) => {
      $(span)
        .before(`<sup>${i + 1}</sup>`)
        .appendTo($notes)
        .wrap('<li></li>');
    });
}); 

清单 5.14

插入方法回调

反向插入方法可以接受一个函数作为参数,就像.attr().css()一样。这个函数会针对每个目标元素调用一次,并且应返回要插入的 HTML 字符串。我们可以在这里使用这种技术,但由于我们将遇到每个脚注的几种这样的情况,因此单个的.each()调用最终将成为更清晰的解决方案。

现在我们准备处理我们清单中的最后一步:为文本位置创建到相应脚注的链接,以及从脚注返回到文本位置。为了实现这一点,我们需要每个脚注四个标记:在文本中和脚注之后各一个链接,以及在相同位置的两个id属性。因为.before()方法的参数即将变得复杂,这是一个引入新的字符串创建的好时机。

清单 5.14 中,我们使用模板字符串准备了我们的脚注标记。这是一种非常有用的技术,但是当连接大量字符串时,它可能开始显得混乱。相反,我们可以使用数组方法.join()来构建更大的字符串。以下语句具有相同的效果:

var str = 'a' + 'b' + 'c'; 
var str = `${'a'}${'b'}${'c'}`;
var str = ['a', 'b', 'c'].join(''); 

尽管在这个例子中需要输入更多的字符,但.join()方法可以在原本难以阅读的字符串连接或字符串模板时提供清晰度。让我们再次看一下我们的代码,这次使用.join()来创建字符串:

$(() => { 
  const $notes = $('<ol id="notes"></ol>')
    .insertBefore('#footer');

  $('span.footnote')
    .each((i, span) => {
      $(span)
        .before([
          '<sup>',
          i + 1,
          '</sup>'
        ].join(''))
        .appendTo($notes)
        .wrap('<li></li>');
    }); 
}); 

项目清单 5.15

使用这种技术,我们可以为脚注标记增加一个到页面底部的链接,以及一个唯一的id值。一边做这些,我们还将为<li>元素添加一个id,这样链接就有了一个目标,如下面的代码片段所示:

$(() => { 
  const $notes = $('<ol id="notes"></ol>')
    .insertBefore('#footer');

  $('span.footnote')
    .each((i, span) => {
      $(span)
        .before([
          '<a href="#footnote-',
          i + 1,
          '" id="context-',
          i + 1,
          '" class="context">',
          '<sup>',
          i + 1,
          '</sup></a>'
        ].join(''))
        .appendTo($notes)
        .wrap('<li></li>');
    }); 
}); 

项目清单 5.16

在额外的标记放置后,每个脚注标记现在都链接到文档底部的对应脚注。 现在唯一剩下的就是创建一个从脚注返回到其上下文的链接。为此,我们可以使用.appendTo()方法的反向,即.append():

$(() => {
  const $notes = $('<ol id="notes"></ol>')
    .insertBefore('#footer');

  $('span.footnote')
    .each((i, span) => {
      $(span)
        .before([
          '<a href="#footnote-',
          i + 1,
          '" id="context-',
          i + 1,
          '" class="context">',
          '<sup>',
          i + 1,
          '</sup></a>'
        ].join(''))
        .appendTo($notes)
        .append([
          '&nbsp;(<a href="#context-',
          i + 1,
          '">context</a>)'
        ].join(''))
        .wrap('<li></li>');
    }); 
}); 

项目清单 5.17

请注意,href标签指向了对应标记的id值。在下面的屏幕截图中,您可以再次看到脚注,不同的是这次每个脚注后都附加了新链接:

图片

复制元素

到目前为止,在本章中,我们已经插入了新创建的元素,将元素从文档中的一个位置移动到另一个位置,并将新元素包裹在现有元素周围。但是,有时,我们可能想要复制元素。例如,出现在页面页眉中的导航菜单也可以复制并放置在页脚中。每当元素可以被复制以增强页面的视觉效果时,我们可以让 jQuery 承担繁重的工作。

对于复制元素,jQuery 的.clone()方法正是我们需要的;它接受任何匹配元素集并为以后使用创建它们的副本。就像我们前面在本章中探讨过的$()函数的元素创建过程一样,复制的元素在应用插入方法之前不会出现在文档中。

例如,下面的行创建了<div class="chapter">中第一个段落的副本:

$('div.chapter p:eq(0)').clone(); 

光靠这些还不足以改变页面的内容。我们可以使克隆的段落出现在<div class="chapter">之前用插入方法:

$('div.chapter p:eq(0)')
  .clone()
  .insertBefore('div.chapter'); 

这将导致第一个段落出现两次。因此,使用一个熟悉的类比,.clone() 与插入方法的关系就像 复制粘贴 一样。

带事件的克隆

默认情况下,.clone() 方法不会复制绑定到匹配元素或其任何后代的任何事件。然而,它可以接受一个布尔参数(当设置为 true(.clone(true))时),也会克隆事件。这种方便的事件克隆使我们避免了手动重新绑定事件,正如 第三章 中讨论的那样,处理事件

用于引文的克隆

许多网站,就像它们的印刷对应物一样,使用 引文 来强调文本的小部分并吸引读者的注意。引文简单地是主文档的摘录,它以特殊的图形处理与文本一起呈现。我们可以通过 .clone() 方法轻松实现这种修饰。首先,让我们再次看一下示例文本的第三段:

<p> 
  <span class="pull-quote">It is a Law of Nature  
  <span class="drop">with us</span> that a male child shall  
  have <strong>one more side</strong> than his father</span>,  
  so that each generation shall rise (as a rule) one step in  
  the scale of development and nobility. Thus the son of a  
  Square is a Pentagon; the son of a Pentagon, a Hexagon; and  
  so on. 
</p> 

注意段落以 <span class="pull-quote"> 开始。这是我们将要复制的类。一旦在另一个位置粘贴了该 <span> 标签中的复制文本,我们就需要修改其样式属性以使其与其余文本区分开。

为了实现这种类型的样式,我们将在复制的 <span> 中添加一个 pulled 类。在我们的样式表中,该类接收以下样式规则:

.pulled { 
  position: absolute; 
  width: 120px; 
  top: -20px; 
  right: -180px; 
  padding: 20px; 
  font: italic 1.2em "Times New Roman", Times, serif; 
  background: #e5e5e5; 
  border: 1px solid #999; 
  border-radius: 8px; 
  box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.6); 
} 

具有此类的元素通过应用背景、边框、字体等样式规则在视觉上与主内容区分开来。最重要的是,它是绝对定位的,距离 DOM 中最近的(absoluterelative)定位的祖先元素的顶部 20 像素,并且向右偏移 20 像素。如果没有祖先元素应用了定位(除了 static 之外),引用的引用将相对于文档 <body> 定位。因此,在 jQuery 代码中,我们需要确保克隆的引文的父元素设置了 position:relative

CSS 定位计算

尽管顶部定位相当直观,但可能一开始不清楚引文框将如何定位到其定位父级的右侧 20 像素。我们首先从引文框的总宽度推导数字,这是 width 属性的值加上左右填充的值,或 145px + 5px + 10px = 160px。然后,我们设置引文的 right 属性。一个值为 0 将使引文的右侧与其父元素的右侧对齐。因此,为了将其左侧定位到父元素的右侧 20 像素处,我们需要将其向负方向移动超过其总宽度的 20 像素,即 -180px

现在,我们可以考虑应用此样式所需的 jQuery 代码。我们将从选择器表达式开始,找到所有 <span class="pull-quote"> 元素,并像我们刚讨论的那样为每个父元素应用 position: relative 样式:

$(() => {
  $('span.pull-quote')
    .each((i, span) => {
      $(span)
        .parent()
        .css('position', 'relative');
    });
}); 

列表 5.18

接下来,我们需要创建引用本身,利用我们准备好的 CSS。我们需要克隆每个 <span> 标签,将 pulled 类添加到副本,并将其插入到其父段落的开头:

$(() => { 
  $('span.pull-quote')
    .each((i, span) => {
      $(span)
        .clone()
        .addClass('pulled')
        .prependTo(
          $(span)
            .parent()
            .css('position', 'relative')
        );
    });
}); 

列表 5.19

因为我们在引用处使用了绝对定位,所以它在段落中的位置是无关紧要的。只要它保持在段落内部,根据我们的 CSS 规则,它将相对于段落的顶部和右侧定位。

引用现在出现在其原始段落旁边,正如预期的那样:

这是一个不错的开始。对于我们的下一个改进,我们将稍微清理引用内容。

内容获取器和设置器方法

修改引用并使用省略号来保持内容简洁将是很好的。为了演示这一点,我们在示例文本中的几个单词中包裹了一个 <span class="drop"> 标签。

完成此替换的最简单方法是直接指定要替换旧实体的新 HTML 实体。.html() 方法非常适合这个目的:

$(() => { 
  $('span.pull-quote')
    .each((i, span) => {
      $(span)
        .clone()
        .addClass('pulled')
        .find('span.drop')
          .html('&hellip;')
          .end()
        .prependTo(
          $(span)
            .parent()
            .css('position', 'relative')
        );
    });
}); 

列表 5.20

列表 5.20 中的新行依赖于我们在第二章中学到的 DOM 遍历技巧,选择元素。我们使用 .find() 在引用中搜索任何 <span class="drop"> 元素,对它们进行操作,然后通过调用 .end() 返回到引用本身。在这些方法之间,我们调用 .html() 将内容更改为省略号(使用适当的 HTML 实体)。

在没有参数的情况下调用 .html() 会返回匹配元素内的 HTML 实体的字符串表示。有了参数,元素的内容将被提供的 HTML 实体替换。当使用此技术时,我们必须小心只指定一个有效的 HTML 实体,并正确地转义特殊字符。

指定的单词现已被省略号替换:

引用通常不保留其原始字体格式,比如这个示例中的粗体文本。我们真正想显示的是 <span class="pull-quote"> 的文本,不包含任何 <strong><em><a href> 或其他内联标签。为了将所有引用的 HTML 实体替换为剥离后的仅文本版本,我们可以使用 .html() 方法的伴随方法 .text()

.html() 一样,.text() 方法可以检索匹配元素的内容或用新字符串替换其内容。但与 .html() 不同的是,.text() 总是获取或设置纯文本字符串。当 .text() 检索内容时,所有包含的标签都将被忽略,HTML 实体将被转换为普通字符。当它设置内容时,特殊字符如 < 将被转换为它们的 HTML 实体等价物:

$(() => { 
  $('span.pull-quote')
    .each((i, span) => {
      $(span)
        .clone()
        .addClass('pulled')
        .find('span.drop')
          .html('&hellip;')
          .end()
        .text((i, text) => text)
        .prependTo(
          $(span)
            .parent()
            .css('position', 'relative')
        );
    });
}); 

列表 5.21

使用text()检索值时,会去除标记。这正是我们尝试实现的内容。与你目前学习的其他一些 jQuery 函数一样,text()接受一个函数。返回值用于设置元素的文本,而当前文本则作为第二个参数传入。因此,要从元素文本中删除标记,只需调用text((i, text) => text)。太棒了!

以下是这种方法的结果:

DOM 操作方法简介

jQuery 提供的大量 DOM 操作方法根据任务和目标位置而异。我们在这里没有涵盖所有内容,但大多数都类似于我们已经见过的方法,更多内容将在第十二章,高级 DOM 操作中讨论。以下概要可作为我们可以使用哪种方法来完成哪种任务的提醒:

  • 若要从 HTML 中创建新元素,请使用$()函数

  • 若要每个匹配元素内部插入新元素,请使用以下函数:

    • .append()

    • .appendTo()

    • .prepend()

    • .prependTo()

  • 若要每个匹配元素旁边插入新元素,请使用以下函数:

    • .after()

    • .insertAfter()

    • .before()

    • .insertBefore()

  • 若要每个匹配元素周围插入新元素,请使用以下函数:

    • .wrap()

    • .wrapAll()

    • .wrapInner()

  • 若要用新元素或文本替换每个匹配元素,请使用以下函数:

    • .html()

    • .text()

    • .replaceAll()

    • .replaceWith()

  • 若要在每个匹配元素内部删除元素,请使用以下函数:

    • .empty()
  • 若要删除文档中每个匹配元素及其后代,而实际上不删除它们,请使用以下函数:

    • .remove()

    • .detach()

摘要

在本章中,我们使用 jQuery 的 DOM 修改方法创建、复制、重新组装和美化内容。我们将这些方法应用于单个网页,将一些通用段落转换为带有脚注、拉引用、链接和样式化的文学摘录。这一章向我们展示了使用 jQuery 添加、删除和重新排列页面内容是多么容易。此外,你已经学会了如何对页面元素的 CSS 和 DOM 属性进行任何想要的更改。

接下来,我们将通过 jQuery 的 Ajax 方法进行一次往返旅程到服务器。

进一步阅读

DOM 操作的主题将在第十二章,高级 DOM 操作中进行更详细的探讨。DOM 操作方法的完整列表可在本书的附录 B,快速参考,或在官方 jQuery 文档api.jquery.com/中找到。

练习

挑战练习可能需要使用官方 jQuery 文档http://api.jquery.com/

  1. 改变引入回到顶部链接的代码,使得链接只在第四段后出现。

  2. 当点击回到顶部链接时,在链接后添加一个新段落,其中包含消息“你已经在这里了”。确保链接仍然可用。

  3. 当点击作者的名字时,将其加粗(通过添加元素,而不是操作类或 CSS 属性)。

  4. 挑战:在对加粗的作者名字进行后续点击时,移除已添加的<b>元素(从而在加粗和正常文本之间切换)。

  5. 挑战:对每个章节段落添加一个inhabitants类,而不调用.addClass()。确保保留任何现有的类。

第六章:使用 Ajax 发送数据

术语 Asynchronous JavaScript and XMLAjax)是由 Jesse James Garrett 在 2005 年创造的。此后,它已经代表了许多不同的事物,因为该术语包含了一组相关的能力和技术。在其最基本的层次上,Ajax 解决方案包括以下技术:

  • JavaScript:用于捕获与用户或其他与浏览器相关的事件的交互,并解释来自服务器的数据并在页面上呈现它

  • XMLHttpRequest:这允许在不中断其他浏览器任务的情况下向服务器发出请求

  • 文本数据: 服务器提供的数据格式可以是 XML、HTML 或 JSON 等。

Ajax 将静态网页转变为交互式网络应用程序。毫不奇怪,浏览器在实现XMLHttpRequest对象时并不完全一致,但 jQuery 会帮助我们。

在本章中,我们将涵盖:

  • 在不刷新页面的情况下从服务器加载数据

  • 从浏览器中的 JavaScript 发送数据回服务器

  • 解释各种格式的数据,包括 HTML、XML 和 JSON

  • 向用户提供有关 Ajax 请求状态的反馈

按需加载数据

Ajax 只是一种从服务器加载数据到网络浏览器中而无需刷新页面的方法。这些数据可以采用许多形式,而当数据到达时,我们有许多选项可以处理它。我们将通过使用不同的方法执行相同的基本任务来看到这一点。

我们将构建一个页面,显示按字典条目起始字母分组的条目。定义页面内容区域的 HTML 将如下所示:

<div id="dictionary"> 
</div> 

我们的页面一开始没有内容。我们将使用 jQuery 的各种 Ajax 方法来填充这个 <div> 标记,以显示字典条目。

获取示例代码

您可以从以下 GitHub 仓库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

我们需要一种触发加载过程的方法,所以我们将添加一些链接供我们的事件处理程序依附:

<div class="letters"> 
  <div class="letter" id="letter-a"> 
    <h3><a href="entries-a.html">A</a></h3> 
  </div> 
  <div class="letter" id="letter-b"> 
    <h3><a href="entries-a.html">B</a></h3> 
  </div> 
  <div class="letter" id="letter-c"> 
    <h3><a href="entries-a.html">C</a></h3> 
  </div> 
  <div class="letter" id="letter-d"> 
    <h3><a href="entries-a.html">D</a></h3> 
  </div> 
  <!-- and so on --> 
</div> 

这些简单的链接将带领我们到列出该字母字典条目的页面。我们将采用渐进式增强的方法,允许这些链接在不加载完整页面的情况下操作页面。应用基本样式后,这个 HTML 将产生如下页面:

现在,我们可以专注于将内容放到页面上。

追加 HTML

Ajax 应用程序通常不过是对一块 HTML 的请求。这种技术有时被称为 Asynchronous HTTP and HTMLAHAH),在 jQuery 中几乎很容易实现。首先,我们需要一些要插入的 HTML,我们将其放置在一个名为 a.html 的文件中,与我们的主文档一起。这个辅助 HTML 文件的开头如下:

<div class="entry"> 
  <h3 class="term">ABDICATION</h3> 
  <div class="part">n.</div> 
  <div class="definition"> 
    An act whereby a sovereign attests his sense of the high 
    temperature of the throne. 
    <div class="quote"> 
      <div class="quote-line">Poor Isabella's Dead, whose 
      abdication</div> 
      <div class="quote-line">Set all tongues wagging in the 
      Spanish nation.</div> 
      <div class="quote-line">For that performance 'twere 
      unfair to scold her:</div> 
      <div class="quote-line">She wisely left a throne too 
      hot to hold her.</div> 
      <div class="quote-line">To History she'll be no royal 
      riddle &mdash;</div> 
      <div class="quote-line">Merely a plain parched pea that 
      jumped the griddle.</div> 
      <div class="quote-author">G.J.</div> 
    </div> 
  </div> 
</div> 

<div class="entry"> 
  <h3 class="term">ABSOLUTE</h3> 
  <div class="part">adj.</div> 
  <div class="definition"> 
    Independent, irresponsible.  An absolute monarchy is one 
    in which the sovereign does as he pleases so long as he 
    pleases the assassins.  Not many absolute monarchies are 
    left, most of them having been replaced by limited 
    monarchies, where the sovereign's power for evil (and for 
    good) is greatly curtailed, and by republics, which are 
    governed by chance. 
  </div> 
</div> 

页面继续以这种 HTML 结构的更多条目。单独渲染的话,a.html 看起来相当简单:

请注意,a.html 不是一个真正的 HTML 文档;它不包含 <html><head><body>,这些通常是必需的。我们通常将这样的文件称为部分片段;它的唯一目的是被插入到另一个 HTML 文档中,我们现在将这样做:

$(() => {
  $('#letter-a a')
    .click((e) => {
      e.preventDefault()

      $('#dictionary').load('a.html');
    });
});

第 6.1 节

.load() 方法为我们做了所有繁重的工作。我们使用普通的 jQuery 选择器指定 HTML 片段的目标位置,然后将要加载的文件的 URL 作为参数传递。现在,当单击第一个链接时,文件将被加载并放置在 <div id="dictionary"> 内。一旦插入新的 HTML,浏览器就会渲染它:

注意 HTML 现在已经有样式了,而之前是原样呈现。这是由于主文档中的 CSS 规则;一旦插入新的 HTML 片段,规则也会应用于其元素。

在测试这个示例时,当单击按钮时,字典定义可能会立即出现。这是在本地工作应用程序时的一个危险;很难预测跨网络传输文档时的延迟或中断。假设我们添加一个警报框,在加载定义后显示:

$(() => {
  $('#letter-a a')
    .click((e) => {
      e.preventDefault()

      $('#dictionary').load('a.html');
      alert('Loaded!');
    });
});

第 6.2 节

我们可能会从这段代码的结构中假设警报只能在执行加载后显示。JavaScript 的执行是同步的,严格按顺序一个任务接一个任务执行。

然而,当这段特定的代码在生产 Web 服务器上测试时,由于网络延迟,警报将在加载完成之前出现并消失。这是因为所有 Ajax 调用默认是异步的。异步加载意味着一旦发出检索 HTML 片段的 HTTP 请求,脚本执行立即恢复而不等待。稍后,浏览器收到来自服务器的响应并处理它。这是期望的行为;锁定整个 Web 浏览器等待数据检索是不友好的。

如果必须延迟动作直到加载完成,jQuery 为此提供了一个回调函数。我们已经在第四章中看到了回调,在样式和动画中使用它们在效果完成后执行操作。Ajax 回调执行类似的功能,在从服务器接收数据后执行。我们将在下一个示例中使用此功能,学习如何从服务器读取 JSON 数据。

处理 JavaScript 对象

根据需要按需获取完整形式的 HTML 非常方便,但这意味着必须传输有关 HTML 结构的大量信息以及实际内容。有时我们希望尽可能少地传输数据,并在数据到达后进行处理。在这种情况下,我们需要以 JavaScript 可以遍历的结构检索数据。

借助 jQuery 的选择器,我们可以遍历获取的 HTML 并对其进行操作,但原生 JavaScript 数据格式涉及的数据量较少,处理起来的代码也较少。

检索 JSON

正如我们经常看到的那样,JavaScript 对象只是一组键值对,并且可以用花括号({})简洁地定义。另一方面,JavaScript 数组是用方括号([])即时定义的,并且具有隐式键,即递增整数。结合这两个概念,我们可以轻松表达一些非常复杂和丰富的数据结构。

术语JavaScript 对象表示法JSON)是由 Douglas Crockford 创造的,以利用这种简单的语法。这种表示法可以提供简洁的替代方法来替代臃肿的 XML 格式:

{ 
  "key": "value", 
  "key 2": [ 
    "array", 
    "of", 
    "items" 
  ] 
} 

尽管基于 JavaScript 对象字面量和数组字面量,但 JSON 对其语法要求更具规范性,对其允许的值更具限制性。例如,JSON 指定所有对象键以及所有字符串值必须用双引号括起来。此外,函数不是有效的 JSON 值。由于其严格性,开发人员应避免手动编辑 JSON,而应依赖于诸如服务器端脚本之类的软件来正确格式化它。

有关 JSON 的语法要求、一些潜在优势以及它在许多编程语言中的实现的信息,请访问json.org/

我们可以以许多方式使用此格式对数据进行编码。为了说明一种方法,我们将一些字典条目放入一个名为 b.json 的 JSON 文件中:

[ 
  { 
    "term": "BACCHUS", 
    "part": "n.", 
    "definition": "A convenient deity invented by the...", 
    "quote": [ 
      "Is public worship, then, a sin,", 
      "That for devotions paid to Bacchus", 
      "The lictors dare to run us in,", 
      "And resolutely thump and whack us?" 
    ], 
    "author": "Jorace" 
  }, 
  { 
    "term": "BACKBITE", 
    "part": "v.t.", 
    "definition": "To speak of a man as you find him when..." 
  }, 
  { 
    "term": "BEARD", 
    "part": "n.", 
    "definition": "The hair that is commonly cut off by..." 
  }, 
  ... file continues ... 

要检索此数据,我们将使用 $.getJSON() 方法,该方法获取文件并对其进行处理。当数据从服务器到达时,它只是一个 JSON 格式的文本字符串。$.getJSON() 方法解析此字符串并向调用代码提供生成的 JavaScript 对象。

使用全局 jQuery 函数

到目前为止,我们使用的所有 jQuery 方法都附加在我们用 $() 函数构建的 jQuery 对象上。选择器允许我们指定一组要处理的 DOM 节点,并且这些方法以某种方式对其进行操作。然而,$.getJSON() 函数是不同的。它没有逻辑 DOM 元素可以应用;结果对象必须提供给脚本,而不是注入到页面中。因此,getJSON() 被定义为全局 jQuery 对象的方法(由 jQuery 库一次定义的单个对象,称为 jQuery$),而不是单个 jQuery 对象实例的方法(由 $() 函数返回的对象)。

如果 $ 是一个类 $.getJSON() 将是一个类方法。对于我们的目的,我们将把这种类型的方法称为全局函数;实际上,它们是使用 jQuery 命名空间的函数,以避免与其他函数名称冲突。

要使用此函数,我们像以前一样将文件名传递给它:

$(() => {
  $('#letter-b a')
    .click((e) => {
      e.preventDefault();
      $.getJSON('b.json');
    });
});

列表 6.3

当我们单击链接时,此代码似乎没有任何效果。函数调用加载文件,但我们还没有告诉 JavaScript 如何处理生成的数据。为此,我们需要使用回调函数。

$.getJSON() 函数接受第二个参数,这是在加载完成时调用的函数。如前所述,Ajax 调用是异步的,回调提供了一种等待数据传输完成而不是立即执行代码的方法。回调函数还接受一个参数,其中填充了生成的数据。所以,我们可以写:

$(() => {
  $('#letter-b a')
    .click((e) => {
      e.preventDefault();
      $.getJSON('b.json', (data) => {});
    });
});

列表 6.4

在这个函数内部,我们可以使用 data 参数根据需要遍历 JSON 结构。我们需要迭代顶级数组,为每个项目构建 HTML。我们将使用数据数组的 reduce() 方法将其转换为 HTML 字符串,然后将其插入文档中。reduce() 方法接受一个函数作为参数,并为数组的每个项返回结果的一部分:

$(() => {
  $('#letter-b a')
    .click((e) => {
      e.preventDefault();

        $.getJSON('b.json', (data) => {
          const html = data.reduce((result, entry) => `
            ${result}
            <div class="entry">
              <h3 class="term">${entry.term}</h3>
              <div class="part">${entry.part}</div>
              <div class="definition">
                ${entry.definition}
              </div>
            </div>
          `, '');

        $('#dictionary')
          .html(html);
    });
  });
});

列表 6.5

我们使用模板字符串来构建每个数组项的 HTML 内容。result 参数是上一个数组项的值。使用这种方法,通过字符串拼接,可以更容易地看到 HTML 结构。一旦为每个条目构建了所有的 HTML,我们就用 .html() 将其插入到 <div id="dictionary"> 中,替换可能已经存在的任何内容。

安全的 HTML

这种方法假定数据对 HTML 消费是安全的;例如,它不应该包含任何杂乱的 < 字符。

唯一剩下的就是处理带引号的条目,我们可以通过实现一对使用 reduce() 技术构建字符串的辅助函数来完成:

$(() => {
  const formatAuthor = entry =>
    entry.author ?
      `<div class="quote-author">${entry.author}</div>` :
      '';

  const formatQuote = entry =>
    entry.quote ?
      `
      <div class="quote">
        ${entry.quote.reduce((result, q) => `
          ${result}
          <div class="quote-line">${q}</div>
        `, '')}
        ${formatAuthor(entry)}
      </div>
      ` : '';

    $('#letter-b a')
      .click((e) => {
        e.preventDefault();

        $.getJSON('b.json', (data) => {
          const html = data.reduce((result, entry) => `
            ${result}
            <div class="entry">
              <h3 class="term">${entry.term}</h3>
              <div class="part">${entry.part}</div>
              <div class="definition">
                ${entry.definition}
                ${formatQuote(entry)}
              </div>
            </div>
          `, '');

          $('#dictionary')
            .html(html);
        });
      });
});

列表 6.6

有了这段代码,我们可以单击 B 链接并确认我们的结果。词典条目如预期的那样显示在页面的右侧:

JSON 格式简洁,但并不宽容。每个括号、大括号、引号和逗号必须存在且被计算在内,否则文件将无法加载。在某些情况下,我们甚至不会收到错误消息;脚本会悄无声息地失败。

执行脚本

有时,我们不希望在页面首次加载时检索到所有将需要的 JavaScript。在某些用户交互发生之前,我们可能不知道需要哪些脚本。我们可以在需要时动态引入 <script> 标签,但更加优雅的注入附加代码的方法是让 jQuery 直接加载 .js 文件。

拉取脚本与加载 HTML 片段一样简单。在这种情况下,我们使用 $.getScript() 函数,它——与其兄弟们一样——接受指向脚本文件的 URL:

$(() => { 
  $('#letter-c a')
    .click((e) => {
      e.preventDefault();
      $.getScript('c.js');
    });
}); 

列表 6.7

在我们的最后一个示例中,我们需要处理结果数据,以便我们可以对加载的文件执行一些有用的操作。不过,对于脚本文件,处理是自动的;脚本只是简单地运行。

以这种方式获取的脚本在当前页面的全局上下文中运行。这意味着它们可以访问所有全局定义的函数和变量,特别是包括 jQuery 本身。因此,我们可以仿照 JSON 示例,在脚本执行时准备和插入 HTML 到页面上,并将此代码放在c.js中:

const entries = [ 
  { 
    "term": "CALAMITY", 
    "part": "n.", 
    "definition": "A more than commonly plain and..." 
  }, 
  { 
    "term": "CANNIBAL", 
    "part": "n.", 
    "definition": "A gastronome of the old school who..." 
  }, 
  { 
    "term": "CHILDHOOD", 
    "part": "n.", 
    "definition": "The period of human life intermediate..." 
  } 
  // and so on 
]; 

const html = entries.reduce((result, entry) => `
  ${result}
  <div class="entry">
    <h3 class="term">${entry.term}</h3>
    <div class="part">${entry.part}</div>
    <div class="definition">
      ${entry.definition}
    </div>
  </div>
`, '');

$('#dictionary')
  .html(html); 

现在,点击 C 链接会得到预期的结果,显示相应的字典条目。

加载 XML 文档

XML 是 Ajax 首字母缩写的一部分,但我们实际上还没有加载任何 XML。这样做很简单,而且与 JSON 技术非常相似。首先,我们需要一个 XML 文件,d.xml,其中包含我们希望显示的一些数据:

<?xml version="1.0" encoding="UTF-8"?> 
<entries> 
  <entry term="DEFAME" part="v.t."> 
    <definition> 
      To lie about another.  To tell the truth about another. 
    </definition> 
  </entry> 
  <entry term="DEFENCELESS" part="adj."> 
    <definition> 
      Unable to attack. 
    </definition> 
  </entry> 
  <entry term="DELUSION" part="n."> 
    <definition> 
      The father of a most respectable family, comprising 
      Enthusiasm, Affection, Self-denial, Faith, Hope, 
      Charity and many other goodly sons and daughters. 
    </definition> 
    <quote author="Mumfrey Mappel"> 
      <line>All hail, Delusion!  Were it not for thee</line> 
      <line>The world turned topsy-turvy we should see; 
        </line> 
      <line>For Vice, respectable with cleanly fancies, 
        </line> 
      <line>Would fly abandoned Virtue's gross advances. 
        </line> 
    </quote> 
  </entry> 
</entries> 

当然,这些数据可以用许多方式表达,有些方式更接近我们早期用于 HTML 或 JSON 的结构。然而,在这里,我们正在说明 XML 的一些特性,以使其对人类更加可读,例如使用termpart属性而不是标签。

我们将以熟悉的方式开始我们的函数:

$(() => {
  $('#letter-d a')
    .click((e) => {
      e.preventDefault();
      $.get('d.xml', (data) => {

      });
    });
}); 

列表 6.8

这次,是$.get()函数完成了我们的工作。通常,此函数只是获取所提供 URL 的文件,并将纯文本提供给回调函数。但是,如果由于其服务器提供的 MIME 类型而已知响应为 XML,则回调函数将交给 XML DOM 树。

幸运的是,正如我们已经看到的,jQuery 具有实质性的 DOM 遍历功能。我们可以像在 HTML 上一样在 XML 文档上使用正常的.find().filter()和其他遍历方法:

$(() => { 
  $('#letter-d a')
    .click((e) => {
      const formatAuthor = entry =>
        $(entry).attr('author') ?
          `
          <div class="quote-author">
            ${$(entry).attr('author')}
          </div>
          ` : '';

      const formatQuote = entry =>
        $(entry).find('quote').length ?
          `
          <div class="quote">
            ${$(entry)
              .find('quote')
              .get()
              .reduce((result, q) => `
                ${result}
                <div class="quote-line">
                  ${$(q).text()}
                </div>
              `, '')}
            ${formatAuthor(entry)}
          </div>
          ` : '';

      e.preventDefault();

      $.get('d.xml', (data) => {
        const html = $(data)
          .find('entry')
          .get()
          .reduce((result, entry) => `
            ${result}
            <div class="entry">
              <h3 class="term">${$(entry).attr('term')}</h3>
              <div class="part">${$(entry).attr('part')}</div>
              <div class="definition">
                ${$(entry).find('definition').text()}
                ${formatQuote(entry)}
              </div>
            </div>
          `, '');

        $('#dictionary')
          .html(html);
      });
    });
}); 

列表 6.9

当点击 D 链接时,这将产生预期的效果:

这是我们已经了解的 DOM 遍历方法的一种新用法,揭示了 jQuery 的 CSS 选择器支持的灵活性。CSS 语法通常用于帮助美化 HTML 页面,因此标准.css文件中的选择器使用 HTML 标签名称(如divbody)来定位内容。然而,jQuery 可以像标准 HTML 一样轻松地使用任意的 XML 标签名称,比如entrydefinition

jQuery 内部的高级选择器引擎使在更复杂的情况下找到 XML 文档的部分变得更加容易。例如,假设我们想将显示的条目限制为具有又带有作者的引用的条目。为此,我们可以通过将entry更改为entry:has(quote)来限制具有嵌套的<quote>元素的条目。然后,我们可以通过编写entry:has(quote[author])来进一步限制具有<quote>元素上的author属性的条目。现在,列表 6.9 中的带有初始选择器的行如下所示:

$(data).find('entry:has(quote[author])').each(function() { 

这个新的选择器表达式相应地限制了返回的条目:

虽然我们可以在从服务器返回的 XML 数据上使用 jQuery,但缺点是我们的代码量已经显著增长。

选择数据格式

我们已经查看了四种用于外部数据的格式,每种格式都由 jQuery 的 Ajax 函数处理。我们还验证了所有四种格式都能够处理手头的任务,在用户请求时加载信息到现有页面上,并且在此之前不加载。那么,我们如何决定在我们的应用程序中使用哪种格式?

HTML 片段 需要非常少的工作来实现。可以使用一个简单的方法将外部数据加载并插入到页面中,甚至不需要回调函数。对于简单的任务,添加新的 HTML 到现有页面中不需要遍历数据。另一方面,数据的结构不一定适合其他应用程序重用。外部文件与其预期的容器紧密耦合。

JSON 文件 结构化简单,易于重用。它们紧凑且易于阅读。必须遍历数据结构以提取信息并在页面上呈现,但这可以通过标准 JavaScript 技术完成。由于现代浏览器可以通过单个调用JSON.parse()原生解析文件,读取 JSON 文件非常快速。JSON 文件中的错误可能导致静默失败,甚至在页面上产生副作用,因此数据必须由可信任的方进行精心制作。

JavaScript 文件 提供了最大的灵活性,但实际上并不是一种数据存储机制。由于文件是特定于语言的,因此无法用于向不同的系统提供相同的信息。相反,加载 JavaScript 文件的能力意味着很少需要的行为可以拆分到外部文件中,减少代码大小,直到需要为止。

尽管 XML 在 JavaScript 社区中已经不再受欢迎,大多数开发人员更喜欢 JSON,但它仍然如此普遍,以至于以此格式提供数据很可能使数据在其他地方得到重用。XML 格式有点臃肿,解析和操作速度可能比其他选项慢一些。

考虑到这些特点,通常最容易将外部数据提供为 HTML 片段,只要数据不需要在其他应用程序中使用。在数据将被重用但其他应用程序也可能受到影响的情况下,由于其性能和大小,JSON 通常是一个不错的选择。当远程应用程序未知时,XML 可能提供最大的保证,可以实现互操作性。

比起其他任何考虑因素,我们应确定数据是否已经可用。如果是,那么很可能最初就是以其中一种这种格式呈现的,因此决策可能已经为我们做出。

向服务器传递数据

到目前为止,我们的示例重点放在从 Web 服务器检索静态数据文件的任务上。但是,服务器可以根据来自浏览器的输入动态地塑造数据。在这项任务中,jQuery 也为我们提供了帮助;我们迄今为止介绍的所有方法都可以修改,以便数据传输变成双向街道。

与服务器端代码交互

由于演示这些技术需要与 Web 服务器进行交互,所以我们将在这里首次使用服务器端代码。给出的示例将使用 Node.js,它非常广泛使用并且免费提供。我们不会在这里涵盖任何 Node.js 或 Express 的具体内容,但是如果你搜索这两项技术,网络上有丰富的资源可供参考。

执行 GET 请求

为了说明客户端(使用 JavaScript)与服务器(同样使用 JavaScript)之间的通信,我们将编写一个脚本,每次请求只向浏览器发送一个词典条目。所选择的条目将取决于从浏览器发送的参数。我们的脚本将从类似于这样的内部数据结构中获取数据:

const E_entries = {
  EAVESDROP: {
    part: 'v.i.',
    definition: 'Secretly to overhear a catalogue of the ' +
                'crimes and vices of another or yourself.',
    quote: [
      'A lady with one of her ears applied',
      'To an open keyhole heard, inside,',
      'Two female gossips in converse free &mdash;',
      'The subject engaging them was she.',
      '"I think," said one, "and my husband thinks',
      'That she's a prying, inquisitive minx!"',
      'As soon as no more of it she could hear',
      'The lady, indignant, removed her ear.',
      '"I will not stay," she said, with a pout,',
      '"To hear my character lied about!"',
    ],
    author: 'Gopete Sherany',
  },
  EDIBLE: {
    part:'adj.',
    definition: 'Good to eat, and wholesome to digest, as ' +
                'a worm to a toad, a toad to a snake, a snake ' +
                'to a pig, a pig to a man, and a man to a worm.',
  },
  // Etc...

在这个示例的生产版本中,数据可能会存储在数据库中,并根据需要加载。由于数据在这里是脚本的一部分,所以检索它的代码非常简单。我们检查 URL 的查询字符串部分,然后将术语和条目传递给一个返回 HTML 片段以显示的函数:

const formatAuthor = entry =>
  entry.author ?
    `<div class="quote-author">${entry.author}</div>` :
    '';

const formatQuote = entry =>
  entry.quote ?
    `
    <div class="quote">
      ${entry.quote.reduce((result, q) => `
        ${result}
        <div class="quote-line">${q}</div>
      `, '')}
      ${formatAuthor(entry)}
    </div>
    ` : '';

const formatEntry = (term, entry) => `
  <div class="entry">
    <h3 class="term">${term}</h3>
    <div class="part">${entry.part}</div>
    <div class="definition">
      ${entry.definition}
      ${formatQuote(entry)}
    </div>
  </div>
`;

app.use(express.static('./'));

app.get('/e', (req, res) => {
  const term = req.query.term.toUpperCase();
  const entry = E_entries[term];
  let content;

  if (entry) {
    content = formatEntry(term, entry);
  } else {
    content = '<div>Sorry, your term was not found.</div>';
  }

  res.send(content);
}); 

现在,对这个 /e 处理器的请求,将返回对应于在 GET 参数中发送的术语的 HTML 片段。例如,当使用 /e?term=eavesdrop 访问处理器时,我们会得到:

再次注意我们之前看到的 HTML 片段缺乏格式,因为尚未应用 CSS 规则。

由于我们正在展示数据如何传递到服务器,所以我们将使用不同的方法来请求条目,而不是迄今为止所依赖的孤立按钮。相反,我们将为每个术语呈现一个链接列表,并且点击任何一个链接都将加载相应的定义。我们将添加以下 HTML:

<div class="letter" id="letter-e"> 
  <h3>E</h3> 
  <ul> 
    <li><a href="e?term=Eavesdrop">Eavesdrop</a></li> 
    <li><a href="e?term=Edible">Edible</a></li> 
    <li><a href="e?term=Education">Education</a></li> 
    <li><a href="e?term=Eloquence">Eloquence</a></li> 
    <li><a href="e?term=Elysium">Elysium</a></li> 
    <li><a href="e?term=Emancipation">Emancipation</a> 
      </li> 
    <li><a href="e?term=Emotion">Emotion</a></li> 
    <li><a href="e?term=Envelope">Envelope</a></li> 
    <li><a href="e?term=Envy">Envy</a></li> 
    <li><a href="e?term=Epitaph">Epitaph</a></li> 
    <li><a href="e?term=Evangelist">Evangelist</a></li> 
  </ul> 
</div> 

现在,我们需要让我们的前端 JavaScript 代码调用后端 JavaScript,并传递正确的参数。我们可以使用正常的 .load() 机制来做到这一点,直接将查询字符串附加到 URL 并使用类似于 e?term=eavesdrop 的地址获取数据。但是,我们可以让 jQuery 根据我们提供给 $.get() 函数的对象构造查询字符串:

$(() => { 
  $('#letter-e a')
    .click((e) => {
      e.preventDefault();

      const requestData = {
        term: $(e.target).text()
      };

      $.get('e', requestData, (data) => {
        $('#dictionary').html(data);
      });
    });
}); 

列表 6.10

现在我们已经看到 jQuery 提供的其他 Ajax 接口,这个函数的操作看起来很熟悉。唯一的区别是第二个参数,它允许我们提供一个包含键和值的对象,这些键和值成为查询字符串的一部分。在这种情况下,键始终是 term,但值是从每个链接的文本中获取的。现在,点击列表中的第一个链接会显示其定义:

这里的所有链接都有 URL,即使我们在代码中没有使用它们。为了防止链接在点击时正常跟随,我们调用.preventDefault()方法。

返回 false 还是阻止默认行为?

在本章中编写 click 处理程序时,我们选择使用 e.preventDefault() 而不是以 return false 结束处理程序。当默认操作否则会重新加载页面或加载另一页时,建议采用这种做法。例如,如果我们的 click 处理程序包含 JavaScript 错误,调用处理程序的第一行.preventDefault()(在遇到错误之前)确保表单不会被提交,并且我们浏览器的错误控制台将正确报告错误。请记住,从 第三章 处理事件return false 调用了 event.preventDefault()event.stopPropagation()。如果我们想要阻止事件冒泡,我们还需要调用后者。

序列化表单

将数据发送到服务器通常涉及用户填写表单。与其依赖于正常的表单提交机制,该机制将在整个浏览器窗口中加载响应,我们可以使用 jQuery 的 Ajax 工具包异步提交表单并将响应放置在当前页面中。

要尝试这个,我们需要构建一个简单的表单:

<div class="letter" id="letter-f"> 
  <h3>F</h3> 
  <form action="f"> 
    <input type="text" name="term" value="" id="term" /> 
    <input type="submit" name="search" value="search" 
      id="search" /> 
  </form> 
</div> 

这一次,我们将通过使我们的 /f 处理程序搜索提供的搜索词作为字典词的子字符串来从服务器返回一组条目。我们将使用我们从 /e 处理程序 中的 formatEntry() 函数以与之前相同的格式返回数据。以下是 /f 处理程序的实现:

app.post('/f', (req, res) => {
  const term = req.body.term.toUpperCase();
  const content = Object.keys(F_entries)
    .filter(k => k.includes(term))
    .reduce((result, k) => `
      ${result}
      ${formatEntry(k, F_entries[k])}
    `, '');

  res.send(content);
}); 

现在,我们可以对表单提交做出反应,并通过遍历 DOM 树来制作正确的查询参数:

$(() => {
  $('#letter-f form')
    .submit((e) => {
      e.preventDefault();

      $.post(
        $(e.target).attr('action'),
        { term: $('input[name="term"]').val() },
        (data) => { $('#dictionary').html(data); }
      );
    });
}); 

清单 6.11

此代码具有预期效果,但按名称搜索输入字段并逐个将其附加到地图中是繁琐的。特别是,随着表单变得更加复杂,这种方法的扩展性不佳。幸运的是,jQuery 提供了一个经常使用的惯用语的快捷方式。.serialize() 方法作用于 jQuery 对象,并将匹配的 DOM 元素转换为可以与 Ajax 请求一起传递的查询字符串。我们可以将我们的提交处理程序概括如下:

$(() => {
  $('#letter-f form')
    .submit((e) => {
      e.preventDefault();

      $.post(
        $(e.target).attr('action'),
        $(e.target).serialize(),
        (data) => { $('#dictionary').html(data); }
      );
    }); 
}); 

清单 6.12

同样的脚本将用于提交表单,即使字段数量增加。例如,当我们搜索 fid 时,包含该子字符串的术语会显示如下屏幕截图所示:

注意请求

到目前为止,我们只需调用一个 Ajax 方法并耐心等待响应就足够了。然而,有时候,了解 HTTP 请求在进行中的情况会很方便。如果出现这种需要,jQuery 提供了一套函数,可以在发生各种与 Ajax 相关的事件时注册回调函数。

.ajaxStart().ajaxStop() 方法是这些观察者函数的两个示例。当没有其他传输正在进行时开始一个 Ajax 调用时,将触发 .ajaxStart() 回调。相反,当最后一个活动请求结束时,将执行与 .ajaxStop() 绑定的回调。所有观察者都是全局的,它们在发生任何 Ajax 通信时被调用,无论是什么代码启动的。而且所有这些观察者只能绑定到 $(document)

我们可以利用这些方法在网络连接缓慢的情况下向用户提供一些反馈。页面的 HTML 可以附加适当的加载消息:

<div id="loading"> 
  Loading... 
</div> 

这个消息只是一段任意的 HTML 代码;例如,它可以包含一个动画 GIF 图像作为加载指示器。在这种情况下,我们将在 CSS 文件中添加一些简单的样式,以便在显示消息时,页面看起来如下:

为了符合渐进增强的精神,我们不会直接将这个 HTML 标记放在页面上。相反,我们将使用 jQuery 插入它:

$(() => {
  $('<div/>')
    .attr('id', 'loading')
    .text('Loading...')
    .insertBefore('#dictionary');
}); 

我们的 CSS 文件将给这个 <div> 添加一个 display: none; 的样式声明,以便最初隐藏消息。在适当的时候显示它,我们只需使用 .ajaxStart() 将其注册为观察者:

$(() => {
  const $loading = $('<div/>')
    .attr('id', 'loading')
    .text('Loading...')
    .insertBefore('#dictionary');

  $(document)
    .ajaxStart(() => {
      $loading.show(); 
    }); 
}); 

我们可以将隐藏行为链接在一起:

$(() => {
  const $loading = $('<div/>')
    .attr('id', 'loading')
    .text('Loading...') 
    .insertBefore('#dictionary'); 

  $(document)
    .ajaxStart(() => {
      $loading.show(); 
    })
    .ajaxStop(() => {
      $loading.hide(); 
    }); 
}); 

列表 6.13

现在我们有了加载反馈。

再次说明,这些方法与 Ajax 通信开始的具体方式无关。附加到 A 链接的 .load() 方法和附加到 B 链接的 .getJSON() 方法都会导致这些操作发生。

在这种情况下,这种全局行为是可取的。不过,如果我们需要更具体的行为,我们有几个选择。一些观察者方法,比如 .ajaxError(),会将它们的回调函数发送给 XMLHttpRequest 对象的引用。这可以用于区分一个请求和另一个请求,并提供不同的行为。通过使用低级别的 $.ajax() 函数,我们可以实现其他更具体的处理,稍后我们会讨论这个函数。

与请求交互的最常见方式是 success 回调,我们已经介绍过了。我们在几个示例中使用它来解释从服务器返回的数据,并用结果填充页面。当然,它也可以用于其他反馈。再次考虑我们从 列表 6.1 中的 .load() 示例:

$(() => { 
  $('#letter-a a')
    .click((e) => {
      e.preventDefault();
      $('#dictionary')
        .load('a.html'); 
    }); 
}); 

我们可以通过使加载的内容淡入而不是突然出现来进行一点小改进。.load() 方法可以接受一个回调函数在完成时被触发:

$(() => { 
  $('#letter-a a')
    .click((e) => {
      e.preventDefault();
      $('#dictionary')
        .hide()
        .load('a.html', function() { 
          $(this).fadeIn(); 
        }); 
    }); 
}); 

列表 6.14

首先,我们隐藏目标元素,然后开始加载。加载完成后,我们使用回调函数将新填充的元素显示出来,以淡入的方式。

错误处理

到目前为止,我们只处理了 Ajax 请求的成功响应,当一切顺利时加载页面以显示新内容。然而,负责任的开发人员应考虑网络或数据错误的可能性,并适当地报告它们。在本地环境中开发 Ajax 应用程序可能会使开发人员产生自满感,因为除了可能的 URL 输入错误外,Ajax 错误不会在本地发生。Ajax 方便的方法,如$.get().load()本身不提供错误回调参数,因此我们需要寻找其他地方解决此问题。

除了使用global .ajaxError()方法外,我们还可以利用 jQuery 的延迟对象系统来对错误做出反应。我们将在第十一章,高级效果中更详细地讨论延迟对象,但是,现在我们简单地指出,我们可以将.done().always().fail()方法链接到除.load()之外的任何 Ajax 函数,并使用这些方法来附加相关的回调。例如,如果我们取自列表 6.16的代码,并将 URL 更改为不存在的 URL,我们可以测试.fail()方法:

$(() => { 
  $('#letter-e a')
    .click((e) => {
      e.preventDefault();

      const requestData = {
        term: $(e.target).text()
      };

      $.get('notfound', requestData, (data) => {
        $('#dictionary').html(data);
      }).fail((xhr) => {
        $('#dictionary')
          .html(`An error occurred:
            ${xhr.status}
            ${xhr.responseText}
          `);
      });
    });
}); 

列表 6.15

现在,点击以 E 开头的任何术语链接都会产生错误消息。jqXHR.responseText的确切内容将根据服务器配置的不同而变化:

.status属性包含服务器提供的数字代码。这些代码在 HTTP 规范中定义,当触发.fail()处理程序时,它们将代表错误条件,例如:

响应代码 描述
400 错误请求
401 未经授权
403 禁止访问
404 未找到
500 内部服务器错误

可以在 W3C 的网站上找到完整的响应代码列表:www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

我们将更仔细地检查错误处理在第十三章,高级 Ajax中。

Ajax 和事件

假设我们想允许每个词典术语名称控制其后跟的定义的显示;点击术语名称将显示或隐藏相关定义。根据我们目前所见的技术,这应该是相当简单的:

$(() => {
  $('h3.term')
    .click((e) => {
      $(e.target)
        .siblings('.definition')
        .slideToggle();
    });
}); 

列表 6.16

当点击术语时,此代码会查找具有definition类的元素的兄弟元素,并根据需要将它们上下滑动。

一切看起来都井然有序,但此代码不起作用。不幸的是,当我们附加click处理程序时,术语尚未添加到文档中。即使我们设法将click处理程序附加到这些项上,一旦我们点击不同的字母,处理程序将不再附加。

这是一个常见的问题,当页面的某些区域由 Ajax 填充时。一个流行的解决方案是每次页面区域被刷新时重新绑定处理程序。然而,这可能很麻烦,因为每次任何事情导致页面的 DOM 结构发生变化时,事件绑定代码都需要被调用。

更优的选择在第三章,事件处理中被介绍。我们可以实现事件委托,实际上将事件绑定到一个永远不会改变的祖先元素上。在这种情况下,我们将click处理程序附加到<body>元素上,使用.on()这样来捕获我们的点击:

$(() => { 
  $('body')
    .on('click', 'h3.term', (e) => {
      $(e.target)
        .siblings('.definition')
        .slideToggle();
    });
}); 

第 6.17 节

当以这种方式使用时,.on()方法告诉浏览器在整个文档中观察所有点击。如果(且仅当)点击的元素与h3.term选择器匹配,则执行处理程序。现在,切换行为将在任何术语上发生,即使它是由后来的 Ajax 事务添加的。

延迟对象和承诺

在 JavaScript 代码中处理异步行为时,jQuery 延迟对象是在没有一致的方式时引入的。承诺帮助我们编排异步事务,如多个 HTTP 请求、文件读取、动画等。承诺不是 JavaScript 专有的,也不是一个新的想法。将承诺视为一个承诺最终解析值的合同是最好的理解方式。

现在承诺已经正式成为 JavaScript 的一部分,jQuery 现在完全支持承诺。也就是说,jQuery 延迟对象的行为与任何其他承诺一样。这很重要,因为我们将在本节中看到,这意味着我们可以使用 jQuery 延迟对象来与返回原生承诺的其他代码组合复杂的异步行为。

在页面加载时执行 Ajax 调用

现在,我们的字典在初始页面加载时不显示任何定义。相反,它只显示一些空白空间。让我们通过在文档准备好时显示"A"条目来改变这种情况。我们如何做到这一点?

一种方法是简单地将load('a.html')调用添加到我们的文档准备处理程序($(() => {}))中,以及其他所有内容。问题在于这样效率低下,因为我们必须等待文档准备好才能发出 Ajax 请求。如果我们的 JavaScript 一运行就发出 Ajax 请求会不会更好呢?

挑战在于将文档准备事件与 Ajax 响应准备事件同步。这里存在竞争条件,因为我们不知道哪个事件会先发生。文档准备可能会首先完成,但我们不能做出这种假设。这就是承诺非常有用的地方:

Promise.all([
  $.get('a.html'),
  $.ready
]).then(([content]) => {
  $('#dictionary')
    .hide()
    .html(content)
    .fadeIn();
});

第 6.18 节

Promise.all()方法接受其他 promise 的数组,并返回一个新的 promise。当数组参数中的所有内容都解析了,这个新的 promise 就解析了。这就是 promise 为我们处理异步竞争条件的方式。无论 Ajax promise ($.get('a.html'))先解析还是文档准备好 promise ($.ready)先解析,都不重要。

then()处理程序是我们想要执行依赖于异步值的任何代码的地方。例如,content 值是解析后的 Ajax 调用。文档准备好隐式解析了 DOM。如果 DOM 没有准备好,我们就不能运行$('#dictionary')...

使用 fetch()

JavaScript 的另一个近期新增功能是fetch()函数。这是XMLHttpRequest的更灵活的替代品。例如,当进行跨域请求时或需要调整特定的 HTTP 头值时,使用fetch()更加容易。让我们使用fetch()来实现G条目:

$(() => {
  $('#letter-g a')
    .click((e) => { 
      e.preventDefault();

      fetch('/g')
        .then(resp => resp.json())
        .then(data => {
          const html = data.reduce((result, entry) => `
            ${result}
            <div class="entry">
              <h3 class="term">${entry.term}</h3>
              <div class="part">${entry.part}</div>
              <div class="definition">
                ${entry.definition}
                ${formatQuote(entry)}
              </div>
            </div>
          `, '');

          $('#dictionary')
            .html(html);
        });
    });
});

列表 6.19

fetch()函数返回一个 promise,就像各种 jQuery Ajax 函数一样。这意味着如果我们在这个例子中调用的/g网址实际上位于另一个域中,我们可以使用fetch()来访问它。如果我们需要 JSON 数据,我们需要在.then()处理程序中调用.json()。然后,在第二个处理程序中,我们可以使用在本章前面创建的相同函数来填充 DOM。

Promise 背后的整个理念是一致性。如果我们需要同步异步行为,promise 是解决的方法。任何 jQuery 异步执行的内容,都可以使用其他 promise。

总结

你已经学会了 jQuery 提供的 Ajax 方法可以帮助我们从服务器加载多种不同格式的数据,而无需页面刷新。我们可以根据需要从服务器执行脚本,并将数据发送回服务器。

你还学会了如何处理异步加载技术的常见挑战,比如在加载完成后保持处理程序的绑定以及从第三方服务器加载数据。

这结束了我们对jQuery库基本组件的介绍。接下来,我们将看看这些功能如何通过 jQuery 插件轻松扩展。

进一步阅读

Ajax 的主题将在第十三章 高级 Ajax中更详细地探讨。完整的 Ajax 方法列表可以在本书的附录 B 快速参考或官方的 jQuery 文档中找到 api.jquery.com/

练习

挑战性的练习可能需要使用官方的 jQuery 文档

api.jquery.com/:

  1. 当页面加载时,将exercises-content.html的内容填充到页面的内容区域。

  2. 而不是一次性显示整个文档,当用户将鼠标悬停在左侧列中的字母上时,通过从exercises-content.html加载适当字母的内容,创建工具提示。

  3. 为这个页面加载添加错误处理,将错误消息显示在内容区域。通过将脚本更改为请求does-not-exist.html而不是exercises-content.html来测试这个错误处理代码。

  4. 这是一个挑战。页面加载时,向 GitHub 发送一个 JSONP 请求,并检索用户的存储库列表。将每个存储库的名称和网址插入页面的内容区域。检索 jQuery 项目存储库的网址是api.github.com/users/jquery/repos

第七章:使用插件

在本书的前六章中,我们审视了 jQuery 的核心组件。这样做已经说明了 jQuery 库可以用来完成各种任务的许多方法。尽管库在其核心处非常强大,但其优雅的插件架构使开发人员能够扩展 jQuery,使其功能更加丰富。

jQuery 社区创建了数百个插件——从小的选择器辅助工具到完整的用户界面部件。现在,您将学习如何利用这一庞大资源。

在本章中,我们将介绍:

  • 下载和设置插件

  • 调用插件提供的 jQuery 方法

  • 使用由 jQuery 插件定义的自定义选择器查找元素

  • 使用 jQuery UI 添加复杂的用户界面行为

  • 使用 jQuery Mobile 实现移动友好功能

使用插件

使用 jQuery 插件非常简单。我们只需要获取插件代码,从我们的 HTML 中引用插件,并从我们自己的脚本中调用新的功能。

我们可以使用 jQuery Cycle 插件轻松演示这些任务。这个由 Mike Alsup 制作的插件可以快速地将静态页面元素集合转换为交互式幻灯片。像许多流行的插件一样,它可以很好地处理复杂的高级需求,但当我们的需求更为简单时,它也可以隐藏这种复杂性。

下载并引用 Cycle 插件

要安装任何 jQuery 插件,我们将使用 npm 包管理器。这是声明现代 JavaScript 项目的包依赖关系的事实上的工具。例如,我们可以使用 package.json 文件声明我们需要 jQuery 和一组特定的 jQuery 插件。

要获取有关安装 npm 的帮助,请参阅 docs.npmjs.com/getting-started/what-is-npm。要获取有关初始化 package.json 文件的帮助,请参阅 docs.npmjs.com/getting-started/using-a-package.json

一旦在项目目录的根目录中有了 package.json 文件,您就可以开始添加依赖项了。例如,您可以从命令控制台如下添加 jquery 依赖项:

npm install jquery --save

如果我们想要使用 cycle 插件,我们也可以安装它:

npm install jquery-cycle --save

我们在此命令中使用 --save 标志的原因是告诉 npm 我们始终需要这些包,并且它应该将这些依赖项保存到 package.json。现在我们已经安装了 jqueryjquery-cycle,让我们将它们包含到我们的页面中:

<head> 
  <meta charset="utf-8"> 
  <title>jQuery Book Browser</title> 
  <link rel="stylesheet" href="07.css" type="text/css" /> 
  <script src="img/jquery.js"></script> 
  <script src="img/index.js"></script> 
  <script src="img/07.js"></script> 
</head>

我们现在已经加载了我们的第一个插件。正如我们所看到的,这不再比设置 jQuery 本身更复杂。插件的功能现在可以在我们的脚本中使用了。

调用插件方法

Cycle 插件可以作用于页面上的任何一组兄弟元素。为了展示它的运行过程,我们将设置一些简单的 HTML,其中包含书籍封面图像和相关信息的列表,并将其添加到我们 HTML 文档的主体中,如下所示:

<ul id="books"> 
  <li> 
    <img src="img/jq-game.jpg" alt="jQuery Game Development  
      Essentials" /> 
    <div class="title">jQuery Game Development Essentials</div> 
    <div class="author">Salim Arsever</div> 
  </li> 
  <li> 
    <img src="img/jqmobile-cookbook.jpg" alt="jQuery Mobile  
      Cookbook" /> 
    <div class="title">jQuery Mobile Cookbook</div> 
    <div class="author">Chetan K Jain</div> 
  </li> 
  ... 
</ul>

在我们的 CSS 文件中进行轻量级样式处理,按照以下截图所示,依次显示书籍封面:

图片

Cycle 插件将会在此列表上发挥其魔力,将其转换为一个引人注目的动画幻灯片。通过在 DOM 中适当的容器上调用 .cycle() 方法,可以调用此转换,如下所示:

$(() => { 
  $('#books').cycle(); 
});

列表 7.1

这种语法几乎没有更简单的了。就像我们使用任何内置的 jQuery 方法一样,我们将 .cycle() 应用于一个 jQuery 对象实例,该实例又指向我们要操作的 DOM 元素。即使没有向它提供任何参数,.cycle() 也为我们做了很多工作。页面上的样式被修改以仅呈现一个列表项,并且每 4 秒使用淡入淡出的转换显示一个新项:

图片

这种简单性是写得很好的 jQuery 插件的典型特征。只需简单的方法调用就能实现专业且有用的结果。然而,像许多其他插件一样,Cycle 提供了大量的选项,用于定制和微调其行为。

指定插件方法参数

将参数传递给插件方法与使用原生 jQuery 方法没有什么不同。在许多情况下,参数被传递为一个键值对的单个对象(就像我们在第六章中看到的 $.ajax()使用 Ajax 发送数据)。提供的选项选择可能会令人生畏;.cycle() 本身就有超过 50 个潜在的配置选项。每个插件的文档详细说明了每个选项的效果,通常还附有详细的示例。

Cycle 插件允许我们改变幻灯片之间的动画速度和样式,影响何时以及如何触发幻灯片转换,并使用回调函数来响应完成的动画。为了演示其中一些功能,我们将从 列表 7.1 的方法调用中提供三个简单的选项,如下所示:

$(() => { 
  $('#books').cycle({ 
    timeout: 2000, 
    speed: 200, 
    pause: true 
  }); 
});

列表 7.2

timeout 选项指定了在每个幻灯片转换之间等待的毫秒数(2,000)。相比之下,speed 决定了转换本身需要花费的毫秒数(200)。当设置为 true 时,pause 选项会导致幻灯片秀在鼠标位于循环区域内时暂停,当循环项可点击时尤其有用。

修改参数默认值

即使没有提供参数,Cycle 插件也是令人印象深刻的。为了实现这一点,当没有提供选项时,它需要一个合理的默认设置来使用。

一种常见的模式,也是 Cycle 遵循的模式,是将所有默认值收集到一个单一对象中。在 Cycle 的情况下,$.fn.cycle.defaults 对象包含所有默认选项。当插件将其默认值收集在像这样的公开可见位置时,我们可以在我们自己的脚本中修改它们。这样可以使我们的代码在多次调用插件时更加简明,因为我们不必每次都指定选项的新值。重新定义默认值很简单,如下面的代码所示:

$.fn.cycle.defaults.timeout = 10000; 
$.fn.cycle.defaults.random = true; 

$(() => { 
  $('#books').cycle({ 
    timeout: 2000, 
    speed: 200, 
    pause: true 
  }); 
});

列表 7.3

在这里,我们在调用.cycle() 之前设置了两个默认值,timeoutrandom。由于我们在.cycle() 中声明了 timeout 的值为 2000,我们的新默认值 10000 会被忽略。另一方面,random 的新默认值为 true 生效,导致幻灯片以随机顺序过渡。

其他类型的插件

插件不仅仅限于提供额外的 jQuery 方法。它们可以在许多方面扩展库甚至改变现有功能的功能。

插件可以改变 jQuery 库的其他部分操作的方式。例如,一些插件提供新的动画缓动样式,或者在用户操作响应中触发额外的 jQuery 事件。Cycle 插件通过添加一个新的自定义选择器来提供这样的增强功能。

自定义选择器

添加自定义选择器表达式的插件会增加 jQuery 内置选择器引擎的功能,使我们可以以新的方式在页面上查找元素。Cycle 添加了这种类型的自定义选择器,这给了我们一个探索这种功能的机会。

通过调用.cycle('pause').cycle('resume'),Cycle 的幻灯片可以暂停和恢复。我们可以轻松地添加控制幻灯片的按钮,如下面的代码所示:

$(() => {
  const $books = $('#books').cycle({
    timeout: 2000,
    speed: 200,
    pause: true
  });
  const $controls = $('<div/>')
    .attr('id', 'books-controls')
    .insertAfter($books);

  $('<button/>')
    .text('Pause')
    .click(() => {
      $books.cycle('pause');
    })
    .appendTo($controls);
  $('<button/>')
    .text('Resume')
    .click(() => {
      $books.cycle('resume');
    })
    .appendTo($controls);
});

列表 7.4

现在,假设我们希望我们的“恢复”按钮恢复页面上任何暂停的 Cycle 幻灯片,如果有多个的话。我们想要找到页面上所有暂停的幻灯片的<ul> 元素,并恢复它们所有。Cycle 的自定义:paused 选择器使我们可以轻松做到这一点:

$(() => { 
  $('<button/>')
    .text('Resume')
    .click(() => {
      $('ul:paused').cycle('resume');
    })
    .appendTo($controls);
});

列表 7.5

使用 Cycle 加载,$('ul:paused') 将创建一个 jQuery 对象,引用页面上所有暂停的幻灯片,以便我们可以随意进行交互。像这样由插件提供的选择器扩展可以自由地与任何标准的 jQuery 选择器结合使用。我们可以看到,选择适当的插件,jQuery 可以被塑造以满足我们的需求。

全局函数插件

许多流行的插件在jQuery命名空间中提供新的全局函数。当插件提供的功能与页面上的 DOM 元素无关,因此不适合标准的 jQuery 方法时,这种模式是常见的。例如,Cookie 插件(github.com/carhartl/jquery-cookie)提供了一个界面,用于在页面上读取和写入 cookie 值。这个功能是通过$.cookie()函数提供的,它可以获取或设置单个 cookie。

比如说,例如,我们想要记住用户什么时候按下我们幻灯片的暂停按钮,以便如果他们离开页面然后过后回来的话我们可以保持它暂停。加载 Cookie 插件之后,读取 cookie 就像在下面代码中一样简单:只需将 cookie 的名称作为唯一参数使用即可。

if ($.cookie('cyclePaused')) { 
  $books.cycle('pause'); 
}

列表 7.6

在这里,我们寻找cyclePaused cookie 的存在;对于我们的目的来说,值是无关紧要的。如果 cookie 存在,循环将暂停。当我们在调用.cycle()之后立即插入这个条件暂停时,幻灯片会一直保持第一张图片可见,直到用户在某个时候按下“恢复”按钮。

当然,因为我们还没有设置 cookie,幻灯片仍在循环播放图片。设置 cookie 和获取它的值一样简单;我们只需像下面这样为第二个参数提供一个字符串:

$(() => {
  $('<button/>')
    .text('Pause')
    .click(() => {
      $books.cycle('pause');
      $.cookie('cyclePaused', 'y');
    })
    .appendTo($controls);
  $('<button/>')
    .text('Resume')
    .click(() => {
      $('ul:paused').cycle('resume');
      $.cookie('cyclePaused', null);
    })
    .appendTo($controls);
});

列表 7.7

当按下暂停按钮时,cookie 被设置为y,当按下恢复按钮时,通过传递null来删除 cookie。默认情况下,cookie 在会话期间保持(通常直到浏览器标签页关闭)。同样默认情况下,cookie 与设置它的页面相关联。要更改这些默认设置,我们可以为函数的第三个参数提供一个选项对象。这是典型的 jQuery 插件模式,也是 jQuery 核心函数。

例如,为了使 cookie 在整个站点上可用,并在 7 天后过期,我们可以调用$.cookie('cyclePaused', 'y', { path: '/', expires: 7 })。要了解在调用$.cookie()时可用的这些和其他选项的信息,我们可以参考插件的文档。

jQuery UI 插件库

虽然大多数插件,比如 Cycle 和 Cookie,都专注于一个单一的任务,jQuery UI 却面对着各种各样的挑战。实际上,虽然 jQuery UI 的代码常常被打包成一个单一的文件,但它实际上是一套相关插件的综合套件。

jQuery UI 团队创建了许多核心交互组件和成熟的小部件,以帮助使网络体验更像桌面应用程序。交互组件包括拖放、排序、选择和调整项的方法。目前稳定的小部件包括按钮、手风琴、日期选择器、对话框等。此外,jQuery UI 还提供了一套广泛的高级效果,以补充核心的 jQuery 动画。

完整的 UI 库过于庞大,无法在本章中充分覆盖;事实上,有整本书专门讨论此主题。幸运的是,该项目的主要焦点是其功能之间的一致性,因此详细探讨几个部分将有助于我们开始使用其余的部分。

所有 jQuery UI 模块的下载、文档和演示都可以在此处找到

jqueryui.com/。下载页面提供了一个包含所有功能的组合下载,或者一个可定制的下载,可以包含我们需要的功能。可下载的 ZIP 文件还包含样式表和图片,我们在使用 jQuery UI 的交互组件和小部件时需要包含它们。

效果

jQuery UI 的效果模块由核心和一组独立的效果组件组成。核心文件提供了颜色和类的动画,以及高级缓动。

颜色动画

将 jQuery UI 的核心效果组件链接到文档中后,.animate() 方法被扩展以接受额外的样式属性,例如 borderTopColorbackgroundColorcolor。例如,我们现在可以逐渐将元素从黑色背景上的白色文本变为浅灰色背景上的黑色文本:

$(() => {
  $books.hover((e) => {
    $(e.target)
      .find('.title')
      .animate({
        backgroundColor: '#eee',
        color: '#000'
      }, 1000);
  }, (e) => {
    $(e.target)
      .find('.title')
      .animate({
        backgroundColor: '#000',
        color: '#fff'
      }, 1000);
  }); 
});

清单 7.8

现在,当鼠标光标进入页面的书籍幻灯片区域时,书名的文本颜色和背景颜色都会在一秒钟(1000 毫秒)的时间内平滑动画过渡:

类动画

我们在前几章中使用过的三个 CSS 类方法--.addClass().removeClass().toggleClass()--被 jQuery UI 扩展为接受可选的第二个参数,用于动画持续时间。当指定了这个持续时间时,页面的行为就像我们调用了 .animate(),并直接指定了应用于元素的类的所有样式属性变化一样:

$(() => {
  $('h1')
    .click((e) => {
      $(e.target).toggleClass('highlighted', 'slow');
    });
});

清单 7.9

通过执行 清单 7.9 中的代码,我们已经导致页面标题的点击添加或删除 highlighted 类。但是,由于我们指定了 slow 速度,结果的颜色、边框和边距变化会以动画形式展现出来,而不是立即生效:

高级缓动

当我们指示 jQuery 在指定的持续时间内执行动画时,它并不是以恒定的速率执行。例如,如果我们调用 $('#my-div').slideUp(1000),我们知道元素的高度将需要整整一秒钟才能达到零;但是,在该秒的开始和结束时,高度将缓慢变化,在中间时将快速变化。这种速率变化被称为缓动,有助于动画看起来平滑自然。

高级缓动函数变化加速和减速曲线,以提供独特的结果。例如,easeInExpo函数呈指数增长,以多倍于开始时的速度结束动画。我们可以在任何核心 jQuery 动画方法或 jQuery UI 效果方法中指定自定义缓动函数。这可以通过添加参数或将选项添加到设置对象中来完成,具体取决于使用的语法。

要查看此示例,请按照以下方式将easeInExpo作为我们刚刚介绍的第 7.9 部分中的.toggleClass()方法的缓动样式提供:

$(() => { 
  $('h1')
    .click((e) => {
      $(e.target)
        .toggleClass(
          'highlighted',
          'slow',
          'easeInExpo'
        );
    });
});

第 7.10 部分

现在,每当单击标题时,通过切换类属性修改的样式都会逐渐出现,然后加速并突然完成过渡。

查看缓动函数的效果

完整的缓动函数集合演示可在

api.jqueryui.com/easings/

其他效果

包含在 jQuery UI 中的单独效果文件添加了各种转换,其中一些可以比 jQuery 本身提供的简单滑动和淡出动画复杂得多。通过调用由 jQuery UI 添加的.effect()方法来调用这些效果。如果需要,可以使用.show().hide()来调用导致元素隐藏或显示的效果。

jQuery UI 提供的效果可以用于多种用途。其中一些,比如transfersize,在元素改变形状和位置时非常有用。另一些,比如explodepuff,提供了吸引人的隐藏动画。还有一些,包括pulsateshake,则将注意力吸引到元素上。

查看效果的实际效果

所有 jQuery UI 效果都在jqueryui.com/effect/#default展示。

shake行为特别适合强调当前不适用的操作。当简历按钮无效时,我们可以在页面上使用这个效果:

$(() => {
  $('<button/>')
    .text('Resume')
    .click((e) => {
      const $paused = $('ul:paused');
      if ($paused.length) {
        $paused.cycle('resume');
        $.cookie('cyclePaused', null);
      } else {
        $(e.target)
          .effect('shake', {
            distance: 10
          });
      }
    })
    .appendTo($controls);
});

第 7.11 部分

我们的新代码检查$('ul:paused')的长度,以确定是否有任何暂停的幻灯片秀要恢复。如果是,则像以前一样调用 Cycle 的resume操作;否则,执行shake效果。在这里,我们看到,与其他效果一样,shake有可用于调整其外观的选项。在这里,我们将效果的distance设置为比默认值小的数字,以使按钮在点击时快速来回摇摆。

交互组件

jQuery UI 的下一个主要功能是其交互组件,这是一组行为,可以用来制作复杂的交互式应用程序。例如,其中一个组件是Resizable,它可以允许用户使用自然的拖动动作改变任何元素的大小。

对元素应用交互就像调用带有其名称的方法一样简单。例如,我们可以通过调用.resizable()来使书名可调整大小,如下所示:

(() => {
  $books
    .find('.title')
    .resizable();
});

列表 7.12

在文档中引用了 jQuery UI 的 CSS 文件后,此代码将在标题框的右下角添加一个调整大小的手柄。拖动此框会改变区域的宽度和高度,如下面的截图所示:

正如我们现在可能期望的那样,这些方法可以使用大量选项进行定制。例如,如果我们希望将调整大小限制为仅在垂直方向上发生,我们可以通过指定应添加哪个拖动手柄来实现如下:

$(() => {
  $books
    .find('.title')
    .resizable({ handles: 's' });
});

列表 7.13

只在区域的南(底部)侧有一个拖动手柄,只能改变区域的高度:

其他交互组件

其他 jQuery UI 交互包括可拖动的、可投放的和可排序的。与可调整大小一样,它们是高度可配置的。我们可以在jqueryui.com/上查看它们的演示和配置选项。

小部件

除了这些基本交互组件外,jQuery UI 还包括一些强大的用户界面小部件,它们的外观和功能像桌面应用程序中我们习惯看到的成熟元素一样。其中一些非常简单。例如,按钮小部件通过吸引人的样式和悬停状态增强了页面上的按钮和链接。

将此外观和行为授予页面上的所有按钮元素非常简单:

$(() => {
  $('button').button(); 
});

列表 7.14

当引用 jQuery UI 平滑主题的样式表时,按钮将具有光滑、有倾斜的外观:

与其他 UI 小部件和交互一样,按钮接受几个选项。例如,我们可能希望为我们的两个按钮提供适当的图标;按钮小部件带有大量预定义的图标供我们使用。为此,我们可以将我们的.button()调用分成两部分,并分别指定每个图标,如下所示:

$(() => {
  $('<button/>')
    .text('Pause')
    .button({
      icons: { primary: 'ui-icon-pause' }
    })
    .click(() => {
      // ...
    })
    .appendTo($controls);
  $('<button/>')
    .text('Resume')
    .button({
      icons: { primary: 'ui-icon-play' }
    })
    .click((e) => {
      // ...
    })
    .appendTo($controls);
});

列表 7.15

我们指定的primary图标对应于 jQuery UI 主题框架中的标准类名。默认情况下,primary图标显示在按钮文本的左侧,而secondary图标显示在右侧:

另一方面,其他小部件要复杂得多。滑块小部件引入了一个全新的表单元素,类似于 HTML5 的范围元素,但与所有流行的浏览器兼容。这支持更高程度的自定义,如下面的代码所示:

$(() => {
  $('<div/>')
    .attr('id', 'slider')
    .slider({
      min: 0,
      max: $books.find('li').length - 1
    })
    .appendTo($controls);
});

列表 7.16

.slider()的调用将一个简单的<div>元素转换为滑块小部件。该小部件可以通过拖动或按箭头键来控制,以帮助实现可访问性:

清单 7.16 中,我们为滑块指定了一个最小值 0,并为幻灯片展示中的最后一本书的索引设置了最大值。我们可以将这个作为幻灯片的手动控制,通过在它们各自的状态改变时在幻灯片和滑块之间发送消息。

为了对滑块值的变化做出反应,我们可以将处理程序绑定到由滑块触发的自定义事件上。这个事件,slide,不是一个原生的 JavaScript 事件,但在我们的 jQuery 代码中表现得像一个。然而,观察这些事件是如此常见,以至于我们可以不需要显式地调用 .on(),而是可以直接将我们的事件处理程序添加到 .slider() 调用本身,如下面的代码所示:

$(() => {
  $('<div/>')
    .attr('id', 'slider')
    .slider({
      min: 0,
      max: $books.find('li').length - 1,
      slide: (e, ui) => {
        $books.cycle(ui.value);
      }
    })
    .appendTo($controls);
});

清单 7.17

每当调用 slide 回调时,它的 ui 参数就会填充有关小部件的信息,包括其当前值。通过将这个值传递给 Cycle 插件,我们可以操作当前显示的幻灯片。

我们还需要在幻灯片向前切换到另一个幻灯片时更新滑块小部件。为了在这个方向上进行通信,我们可以使用 Cycle 的 before 回调,在每次幻灯片转换之前触发:

$(() => {
  const $books = $('#books').cycle({
    timeout: 2000,
    speed: 200,
    pause: true,
    before: (li) => {
      $('#slider')
        .slider(
          'value',
          $('#books li').index(li)
        );
    }
  });
});

清单 7.18

before 回调中,我们再次调用 .slider() 方法。这一次,我们将 value 作为它的第一个参数调用,以设置新的滑块值。在 jQuery UI 的术语中,我们将 value 称为滑块的 方法,尽管它是通过调用 .slider() 方法而不是通过自己的专用方法名称来调用的。

其他小部件

其他 jQuery UI 小部件包括 Datepicker、Dialog、Tabs 和 Accordion。每个小部件都有几个相关的选项、事件和方法。完整列表,请访问

jQuery UI.

jQuery UI ThemeRoller

jQuery UI 库最令人兴奋的功能之一是 ThemeRoller,这是一个基于 Web 的交互式主题引擎,用于 UI 小部件。ThemeRoller 使得创建高度定制、专业外观的元素变得快速简单。我们刚刚创建的按钮和滑块都应用了默认主题;如果没有提供自定义设置,这个主题将从 ThemeRoller 输出:

生成完全不同风格的样式只需简单访问

jqueryui.com/themeroller/,根据需要修改各种选项,然后按下下载主题按钮。然后,可以将样式表和图像的 .zip 文件解压缩到您的站点目录中。例如,通过选择几种不同的颜色和纹理,我们可以在几分钟内为我们的按钮、图标和滑块创建一个新的协调外观,如下面的屏幕截图所示:

jQuery Mobile 插件库

我们已经看到 jQuery UI 如何帮助我们组装即使是复杂 web 应用程序所需的用户界面特性。它克服的挑战是多样且复杂的。然而,当为移动设备设计我们的页面以进行优雅的呈现和交互时,存在一组不同的障碍。为了创建现代智能手机和平板电脑的网站或应用程序,我们可以转向 jQuery Mobile 项目。

与 jQuery UI 一样,jQuery Mobile 由一套相关组件组成,可以单独使用,但可以无缝地一起工作。该框架提供了一个基于 Ajax 的导航系统、移动优化的交互元素和高级触摸事件处理程序。与 jQuery UI 一样,探索 jQuery Mobile 的所有功能是一个艰巨的任务,因此我们将提供一些简单的示例,并参考官方文档了解更多细节。

jQuery Mobile 的下载、文档和演示可在以下位置找到:

jquerymobile.com/.

我们的 jQuery Mobile 示例将使用 Ajax 技术,因此需要网页服务器软件才能尝试这些示例。更多信息可在第六章中找到,使用 Ajax 发送数据

HTML5 自定义 data 属性

到目前为止,在本章中我们看到的代码示例都是使用 JavaScript API 暴露的插件来调用插件功能。我们已经看到了 jQuery 对象方法、全局函数和自定义选择器是插件向脚本提供服务的一些方式。jQuery Mobile 库也有这些入口点,但与其进行交互的最常见方式是使用 HTML5 data 属性。

HTML5 规范允许我们在元素中插入任何我们想要的属性,只要属性以 data- 为前缀。在呈现页面时,这些属性将完全被忽略,但在我们的 jQuery 脚本中可以使用。当我们在页面中包含 jQuery Mobile 时,脚本会扫描页面寻找一些 data-* 属性,并将移动友好的特性添加到相应的元素。

jQuery Mobile 库会寻找几个特定的自定义 data 属性。我们将在第十二章中检查在我们自己的脚本中使用此功能的更一般的方法,高级 DOM 操作

由于这种设计选择,我们将能够演示 jQuery Mobile 的一些强大特性,而无需自己编写任何 JavaScript 代码。

移动导航

jQuery Mobile 最显著的特性之一是它能够将页面上链接的行为简单地转变为 Ajax 驱动的导航。这种转变会为此过程添加简单的动画,同时保留了标准浏览器历史导航。为了看到这一点,我们将从一个呈现有关几本书信息的链接的文档开始(与我们之前用于构建幻灯片放映的相同内容),如下所示:

<!DOCTYPE html>  
<html>  
<head>  
  <title>jQuery Book Browser</title>  
  <link rel="stylesheet" href="booklist.css" type="text/css" /> 
  <script src="img/jquery.js"></script> 
</head>  
<body>  

<div> 
  <div> 
    <h1>Selected jQuery Books</h1> 
  </div> 

  <div> 
    <ul> 
      <li><a href="jq-game.html">jQuery Game Development  
        Essentials</a></li> 
      <li><a href="jqmobile-cookbook.html">jQuery Mobile  
        Cookbook</a></li> 
      <li><a href="jquery-designers.html">jQuery for  
        Designers</a></li> 
      <li><a href="jquery-hotshot.html">jQuery Hotshot</a></li> 
      <li><a href="jqui-cookbook.html">jQuery UI Cookbook</a></li> 
      <li><a href="mobile-apps.html">Creating Mobile Apps with  
        jQuery Mobile</a></li> 
      <li><a href="drupal-7.html">Drupal 7 Development by  
        Example</a></li> 
      <li><a href="wp-mobile-apps.html">WordPress Mobile  
        Applications with PhoneGap</a></li> 
    </ul> 
  </div> 
</div> 

</body> 
</html>

在本章的可下载代码包中,可以在名为mobile.html的文件中找到完成的 HTML 示例页面。

到目前为止,我们还没有介绍 jQuery Mobile,页面呈现出默认的浏览器样式,正如我们所预期的那样。以下是屏幕截图:

我们的下一步是更改文档的<head>部分,以便引用 jQuery Mobile 及其样式表,如下所示:

<head>  
  <title>jQuery Book Browser</title>  
  <meta name="viewport" 
    content="width=device-width, initial-scale=1">  
  <link rel="stylesheet" href="booklist.css"  
    type="text/css" /> 
  <link rel="stylesheet" 
    href="jquery.mobile/jquery.mobile.css" type="text/css" /> 
  <script src="img/jquery.js"></script> 
  <script src="img/jquery-migrate.js"></script>
  <script src="img/jquery.mobile.js"></script> 
</head>

请注意,我们还引入了一个定义页面视口的<meta>元素。这个声明告诉移动浏览器按照完全填充设备宽度的方式缩放文档的内容。

我们必须在页面中包含 jquery-migrate 插件,因为如果没有它,最新稳定版本的 jQuery 就不能与最新稳定版本的 jQuery Mobile 一起工作。想想这个问题。无论如何,一旦这两者正式配合起来,你可以简单地从页面中删除 jquery-migrate 插件。

jQuery Mobile 样式现在应用于我们的文档,显示出更大的无衬线字体,更新颜色和间距,如下图所示:

为了正确处理导航,jQuery Mobile 需要理解我们页面的结构。我们通过使用data-role属性来提供这些信息:

<div data-role="page"> 
  <div data-role="header"> 
    <h1>Selected jQuery Books</h1> 
  </div> 

  <div data-role="content"> 
    <ul> 
      <li><a href="jq-game.html">jQuery Game Development  
        Essentials</a></li> 
      <li><a href="jqmobile-cookbook.html">jQuery Mobile  
        Cookbook</a></li> 
      <li><a href="jquery-designers.html">jQuery for  
        Designers</a></li> 
      <li><a href="jquery-hotshot.html">jQuery Hotshot</a></li> 
      <li><a href="jqui-cookbook.html">jQuery UI Cookbook</a></li> 
      <li><a href="mobile-apps.html">Creating Mobile Apps with  
        jQuery Mobile</a></li> 
      <li><a href="drupal-7.html">Drupal 7 Development by  
        Example</a></li> 
      <li><a href="wp-mobile-apps.html">WordPress Mobile  
        Applications with PhoneGap</a></li> 
    </ul> 
  </div> 
</div>

现在页面加载时,jQuery Mobile 注意到我们有一个页面标题,并在页面顶部渲染出一个标准的移动设备标题栏:

当文本过长时超出标题栏,jQuery Mobile 会截断它,并在末尾添加省略号。在这种情况下,我们可以将移动设备旋转到横向方向以查看完整标题:

更重要的是,为了产生 Ajax 导航,这就是所需的全部内容。在从此列表链接到的页面上,我们使用类似的标记:

<div data-role="page"> 
  <div data-role="header"> 
    <h1>WordPress Mobile Applications with PhoneGap</h1> 
  </div> 
  <div data-role="content"> 
    <img src="img/wp-mobile-apps.jpg" alt="WordPress Mobile  
      Applications with PhoneGap" /> 
    <div class="title">WordPress Mobile Applications with  
      PhoneGap</div> 
    <div class="author">Yuxian Eugene Liang</div> 
  </div> 
</div>

当点击到这个页面的链接时,jQuery Mobile 使用 Ajax 调用加载页面,抓取带有data-role="page"标记的文档部分,并使用淡入过渡显示这些内容:

在一个文档中提供多个页面

除了提供用于加载其他文档的 Ajax 功能外,jQuery Mobile 还提供了在单个文档中包含所有内容时提供相同用户体验的工具。为了实现这一点,我们只需使用标准的#符号将页面中的锚点链接起来,并将页面的那些部分标记为data-role="page",就像它们在单独的文档中一样,如下所示:

<div data-role="page"> 
  <div data-role="header"> 
    <h1>Selected jQuery Books</h1> 
  </div> 

  <div data-role="content"> 
    <ul> 
      <li><a href="#jq-game">jQuery Game Development  
        Essentials</a></li> 
      <li><a href="#jqmobile-cookbook">jQuery Mobile  
        Cookbook</a></li> 
      <li><a href="#jquery-designers">jQuery for  
        Designers</a></li> 
      <li><a href="#jquery-hotshot">jQuery Hotshot</a></li> 
      <li><a href="#jqui-cookbook">jQuery UI Cookbook</a></li> 
      <li><a href="#mobile-apps">Creating Mobile Apps with jQuery  
        Mobile</a></li> 
      <li><a href="#drupal-7">Drupal 7 Development by  
        Example</a></li> 
      <li><a href="wp-mobile-apps.html">WordPress Mobile  
        Applications with PhoneGap</a></li> 
    </ul> 
  </div> 
</div> 

<div id="jq-game" data-role="page"> 
  <div data-role="header"> 
    <h1>jQuery Game Development Essentials</h1> 
  </div> 
  <div data-role="content"> 
    <img src="img/jq-game.jpg" alt="jQuery Game Development  
      Essentials" /> 
    <div class="title">jQuery Game Development Essentials</div> 
    <div class="author">Salim Arsever</div> 
  </div> 
</div>

我们可以根据自己的方便选择这两种技术。将内容放在单独的文档中允许我们延迟加载信息,直到需要时,但这会增加一些开销,因为需要多个页面请求。

交互元素

jQuery Mobile 提供的功能主要是用于页面上的特定交互元素。这些元素增强了基本的网页功能,使页面组件在触摸界面上更加用户友好。其中包括手风琴式可折叠部分、切换开关、滑动面板和响应式表格。

jQuery UI 和 jQuery Mobile 提供的用户界面元素有很大的重叠。不建议在同一页上同时使用这两个库,但由于最重要的小部件都被两者提供,所以很少有这样的需要。

列表视图

由于它们的小型垂直屏幕布局,智能手机应用程序通常是以列表为主导的。我们可以使用 jQuery Mobile 轻松地增强页面上的列表,使它们的行为更像这些常见的本地应用程序元素。再次,我们只需引入 HTML5 自定义数据属性:

<ul data-role="listview" data-inset="true"> 
  <li><a href="#jq-game">jQuery Game Development  
    Essentials</a></li> 
  <li><a href="#jqmobile-cookbook">jQuery Mobile Cookbook</a></li> 
  <li><a href="#jquery-designers">jQuery for Designers</a></li> 
  <li><a href="#jquery-hotshot">jQuery Hotshot</a></li> 
  <li><a href="#jqui-cookbook">jQuery UI Cookbook</a></li> 
  <li><a href="#mobile-apps">Creating Mobile Apps with jQuery  
    Mobile</a></li> 
  <li><a href="#drupal-7">Drupal 7 Development by Example</a></li> 
  <li><a href="wp-mobile-apps.html">WordPress Mobile Applications  
    with PhoneGap</a></li> 
</ul>

添加 data-role="listview" 告诉 jQuery Mobile 将此列表中的链接设为大号,并且易于在触摸界面中用手指激活,而 data-inset="true" 则为列表提供了一个漂亮的边框,将其与周围内容分隔开来。结果是一个熟悉的、具有本地外观的控件,如下所示:

现在,我们有了大型触摸目标,但我们可以再进一步。移动应用程序中的类似列表视图通常会与搜索字段配对,以缩小列表中的项目。我们可以通过引入 data-filter 属性来添加这样一个字段,如下所示:

<ul data-role="listview" data-inset="true" data-filter="true">

结果是一个带有适当图标的圆角输入框,放置在列表上方:

尽管我们没有添加任何自己的代码,但这个搜索字段看起来不仅本地化,而且行为也是正确的:

工具栏按钮

另一个由 jQuery Mobile 增强的用户界面元素是简单按钮。就像 jQuery UI 允许我们标准化按钮外观一样,jQuery Mobile 增加了按钮的大小并修改了外观,以优化它们用于触摸输入。

在某些情况下,jQuery Mobile 甚至会为我们创建适当的按钮,在以前没有的情况下。例如,在移动应用程序的工具栏中通常有按钮。一个标准按钮是屏幕左上角的返回按钮,允许用户向上导航一级。如果我们为页面的 <div> 元素添加 data-add-back-btn 属性,我们就可以在不进行任何脚本工作的情况下获得此功能:

<div data-role="page" data-add-back-btn="true">

一旦添加了这个属性,每次导航到一个页面时,都会在工具栏上添加一个标准的返回按钮:

可以在 jquerymobile.com/ 找到用于初始化和配置 jQuery Mobile 小部件的完整 HTML5 数据属性列表。

高级功能

随着我们的移动页面需要更多定制设计元素和更复杂的交互,jQuery Mobile 提供了强大的工具来帮助我们创建它们。所有功能都在 jQuery Mobile 网站上有文档记录 (jquerymobile.com/)。虽然这些功能在此处详细讨论起来既过于高级又过于繁多,但还是值得简要提及一些:

  • 移动友好事件:当在页面上引用 jQuery Mobile 时,我们的 jQuery 代码可以访问许多特殊事件,包括 taptapholdswipe。对于这些事件的处理程序可以与任何其他事件一样使用.on() 方法进行绑定。特别是对于 tapholdswipe,它们的默认配置(包括触摸持续时间)可以通过访问$.event.special.taphold$.event.special.swipe 对象的属性进行修改。除了基于触摸的事件外,jQuery Mobile 还提供了对滚动、方向更改以及其页面导航的各个阶段以及一组虚拟化鼠标事件的特殊事件的支持,这些事件对鼠标和触摸都做出反应。

  • 主题化:与 jQuery UI 一样,jQuery Mobile 提供了一个 ThemeRoller。

    (jquerymobile.com/themeroller/) 用于自定义小部件的外观和感觉。

  • PhoneGap 集成:使用 jQuery Mobile 构建的站点可以轻松转换为原生移动应用程序,使用 PhoneGap(Cordova),可访问移动设备 API(如相机、加速计和地理位置)和应用商店。$.support.cors$.mobile.allowCrossDomainPages 属性甚至可以允许访问不包含在应用程序中的页面,例如远程服务器上的页面。

总结

在本章中,我们探讨了如何将第三方插件整合到我们的网页中。我们仔细研究了 Cycle 插件、jQuery UI 和 jQuery Mobile,并在此过程中学习了我们将在其他插件中反复遇到的模式。在下一章中,我们将利用 jQuery 的插件架构开发一些不同类型的插件。

练习

  1. 将循环转换持续时间增加到半秒,并更改动画,使每个幻灯片在下一个幻灯片淡出之前淡入。参考循环文档以找到启用此功能的适当选项。

  2. cyclePaused cookie 设置为持续 30 天。

  3. 限制标题框只能以十个像素为增量调整大小。

  4. 让滑块在幻灯片播放时从一个位置平稳地动画到下一个位置。

  5. 不要让幻灯片播放循环无限,使其在显示最后一张幻灯片后停止。当发生这种情况时,禁用按钮和滑块。

  6. 创建一个新的 jQuery UI 主题,具有浅蓝色小部件背景和深蓝色文本,并将主题应用到我们的示例文档中。

  7. 修改mobile.html中的 HTML,使得列表视图按照书名的首字母分隔。详细信息请参阅 jQuery Mobile 文档中关于data-role="list-divider"的部分。

第八章:开发插件

可用的第三方插件提供了丰富的选项来增强我们的编码体验,但有时我们需要更进一步。当我们编写可以被其他人甚至只是我们自己重复使用的代码时,我们可能希望将其打包为一个新的插件。幸运的是,开发插件的过程与编写使用它的代码并没有太大区别。

在本章中,我们将介绍:

  • jQuery命名空间中添加新的全局函数

  • 添加 jQuery 对象方法以允许我们对 DOM 元素进行操作

  • 使用 jQuery UI 小部件工厂创建小部件插件

  • 分发插件

在插件中使用美元($)别名

当我们编写 jQuery 插件时,必须假设 jQuery 库已加载。但是我们不能假设美元(\()别名可用。回顾一下第三章中的内容,*事件处理*,`\).noConflict()方法可以放弃对这个快捷方式的控制。为了解决这个问题,我们的插件应该始终使用完整的 jQuery 名称调用 jQuery 方法,或者在内部定义$`自己。

尤其是在较大的插件中,许多开发人员发现缺少美元符号($)快捷方式使得代码更难阅读。为了解决这个问题,可以通过定义一个函数并立即调用它来为插件的范围定义快捷方式。这种定义并立即调用函数的语法,通常被称为立即调用函数表达式IIFE),看起来像这样:

(($) => { 
  // Code goes here 
})(jQuery); 

包装函数接受一个参数,我们将全局jQuery对象传递给它。参数被命名为$,所以在函数内部我们可以使用美元($)别名而不会出现冲突。

添加新的全局函数

jQuery 的一些内置功能是通过我们一直称为全局函数的方式提供的。正如我们所见,这些实际上是 jQuery 对象的方法,但从实际操作上来说,它们是jQuery命名空间中的函数。

这种技术的一个典型例子是$.ajax()函数。$.ajax()所做的一切都可以通过一个名为ajax()的常规全局函数来实现,但是这种方法会使我们容易遇到函数名冲突。通过将函数放置在jQuery命名空间中,我们只需要担心与其他 jQuery 方法的冲突。这个jQuery命名空间还向那些可能使用插件的人们表明,需要 jQuery 库。

jQuery 核心库提供的许多全局函数都是实用方法;也就是说,它们为经常需要但不难手动完成的任务提供了快捷方式。数组处理函数$.each()$.map()$.grep()就是这样的好例子。为了说明创建这种实用方法,我们将向其中添加两个简单的函数。

要将函数添加到jQuery命名空间中,我们只需将新函数作为jQuery对象的属性赋值即可:

(($) => { 
  $.sum = (array) => { 
    // Code goes here 
  }; 
})(jQuery); 

列表 8.1

现在,在使用此插件的任何代码中,我们可以写:

$.sum(); 

这将像基本函数调用一样工作,并且函数内部的代码将被执行。

这个sum方法将接受一个数组,将数组中的值相加,并返回结果。我们插件的代码相当简洁:

(($) => {
  $.sum = array =>
    array.reduce(
      (result, item) =>
        parseFloat($.trim(item)) + result,
      0
    );
})(jQuery); 

清单 8.2

要计算总和,我们在数组上调用reduce(),它简单地迭代数组中的每个项,并将其添加到result中。在前面的代码中,有两个返回值的回调函数。它们都没有return语句,因为它们是箭头函数。当我们不包括花括号({})时,返回值是隐式的。

为了测试我们的插件,我们将构建一个简单的带有杂货清单的表格:

<table id="inventory"> 
  <thead> 
    <tr class="one"> 
      <th>Product</th> <th>Quantity</th> <th>Price</th> 
    </tr> 
  </thead> 
  <tfoot> 
    <tr class="two" id="sum"> 
      <td>Total</td> <td></td> <td></td> 
    </tr> 
    <tr id="average"> 
      <td>Average</td> <td></td> <td></td> 
    </tr> 
  </tfoot> 
  <tbody> 
    <tr> 
      <td><a href="spam.html" data-tooltip-text="Nutritious and        
      delicious!">Spam</a></td> <td>4</td> <td>2.50</td> 
    </tr> 
    <tr> 
      <td><a href="egg.html" data-tooltip-text="Farm fresh or        
      scrambled!">Egg</a></td> <td>12</td> <td>4.32</td> 
    </tr> 
    <tr> 
      <td><a href="gourmet-spam.html" data-tooltip-text="Chef        
      Hermann's recipe.">Gourmet Spam</a></td> <td>14</td> <td>7.89         
      </td> 
    </tr> 
  </tbody> 
</table> 

获取示例代码

您可以从以下 GitHub 存储库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

现在,我们将编写一个简短的脚本,将适当的表格页脚单元格填充为所有数量的总和:

$(() => {
  const quantities = $('#inventory tbody')
    .find('td:nth-child(2)')
    .map((index, qty) => $(qty).text())
    .get();
  const sum = $.sum(quantities);

  $('#sum')
    .find('td:nth-child(2)')
    .text(sum);
});

清单 8.3

查看呈现的 HTML 页面可验证我们的插件是否正常工作:

添加多个函数

如果我们的插件需要提供多个全局函数,我们可以独立声明它们。在这里,我们将修改我们的插件,添加一个计算数字数组平均值的函数:

(($) => {
  $.sum = array =>
    array.reduce(
      (result, item) =>
        parseFloat($.trim(item)) + result,
      0
    );

  $.average = array =>
    Array.isArray(array) ?
      $.sum(array) / array.length :
      '';
})(jQuery); 

清单 8.4

为了方便和简洁,我们使用$.sum()插件来辅助我们返回$.average()的值。为了减少错误的几率,我们还检查参数以确保其是一个数组,然后再计算平均值。

现在定义了第二种方法,我们可以以相同的方式调用它:

$(() => {
  const $inventory = $('#inventory tbody');
  const prices = $inventory
    .find('td:nth-child(3)')
    .map((index, qty) => $(qty).text())
    .get();
  const average = $.average(prices);

  $('#average')
    .find('td:nth-child(3)')
    .text(average.toFixed(2));
});

清单 8.5

平均值现在显示在第三列中:

扩展全局 jQuery 对象

我们还可以使用$.extend()函数以定义我们的函数的另一种语法:

(($) => {
  $.extend({
    sum: array =>
      array.reduce(
        (result, item) =>
          parseFloat($.trim(item)) + result,
        0
      ),
    average: array =>
      Array.isArray(array) ?
        $.sum(array) / array.length :
        ''
  });
})(jQuery); 

清单 8.6

这样调用时,$.extend()添加或替换全局 jQuery 对象的属性。因此,这与先前的技术产生相同的结果。

在命名空间内隔离函数

现在,我们的插件在jQuery命名空间内创建了两个单独的全局函数。在这里,我们面临一种不同类型的命名空间污染风险;虽然我们仍然可能与其他 jQuery 插件中定义的函数名冲突。为了避免这种情况,最好将给定插件的所有全局函数封装到单个对象中:

(($) => {
  $.mathUtils = {
    sum: array =>
      array.reduce(
        (result, item) =>
          parseFloat($.trim(item)) + result,
        0
      ),
    average: array =>
      Array.isArray(array) ?
        $.mathUtils.sum(array) / array.length :
        ''
  };
})(jQuery); 

清单 8.7

此模式实质上为我们的全局函数创建了另一个命名空间,称为jQuery.mathUtils。虽然我们仍然非正式地称这些函数为全局函数,但它们现在是mathUtils对象的方法,后者本身是全局 jQuery 对象的属性。因此,在我们的函数调用中,我们必须包含插件名称:

$.mathUtils.sum(array); 
$.mathUtils.average(array); 

通过这种技术(和足够独特的插件名称),我们可以在全局函数中避免命名空间冲突。这样,我们就掌握了插件开发的基础知识。将我们的函数保存在名为jquery.mathutils.js的文件中后,我们可以包含此脚本,并在页面上的其他脚本中使用这些函数。

选择命名空间

对于仅供个人使用的功能,将其放置在我们项目的全局命名空间中通常更合理。因此,我们可以选择暴露我们自己的一个全局对象,而不是使用jQuery。例如,我们可以有一个名为ljQ的全局对象,并定义ljQ.mathUtils.sum()ljQ.mathUtils.average()方法,而不是$.mathUtils.sum()$.mathUtils.average()。这样,我们完全消除了选择包含的第三方插件发生命名空间冲突的可能性。

因此,我们现在已经了解了 jQuery 插件提供的命名空间保护和保证库的可用性。然而,这些仅仅是组织上的好处。要真正发挥 jQuery 插件的威力,我们需要学会如何在单个 jQuery 对象实例上创建新方法。

添加 jQuery 对象方法

大多数 jQuery 内置功能是通过其对象实例方法提供的,插件的具有同样出色的表现。每当我们要编写作用于 DOM 一部分的函数时,可能更适合创建一个实例方法

我们已经看到,添加全局函数需要使用jQuery对象扩展新方法。添加实例方法是类似的,但我们要扩展jQuery.fn对象:

jQuery.fn.myMethod = function() { 
  alert('Nothing happens.'); 
}; 

jQuery.fn对象是jQuery.prototype的别名,用于简洁性。

然后,我们可以在使用选择器表达式后,从我们的代码中调用这个新方法:

$('div').myMethod(); 

当我们调用方法时,我们的警报显示(对于文档中的每个<div>都会显示一次)。不过,我们既然没有以任何方式使用匹配的 DOM 节点,我们可能也可以编写一个全局函数。一个合理的方法实现会作用于其上下文。

对象方法上下文

在任何插件方法中,关键字this被设置为当前的 jQuery 对象。因此,我们可以在this上调用任何内置的 jQuery 方法,或者提取其 DOM 节点并对它们进行操作。为了检查我们可以用对象上下文做什么,我们将编写一个小插件来操作匹配元素上的类。

我们的新方法将接受两个类名,并交换每次调用时应用于每个元素的类。虽然 jQuery UI 有一个强大的.switchClass()方法,甚至允许动画地改变类,但我们将提供一个简单的实现作为演示目的:

(function($) {
  $.fn.swapClass = function(class1, class2) {
    if (this.hasClass(class1)) {
      this
        .removeClass(class1)
        .addClass(class2);
    } else if (this.hasClass(class2)) {
      this
        .removeClass(class2)
        .addClass(class1);
    }
  };
})(jQuery);

$(() => {
  $('table')
    .click(() => {
      $('tr').swapClass('one', 'two');
    });
});

图 8.8

在我们的插件中,我们首先测试匹配元素上是否存在class1,如果存在则用class2替换。否则,我们测试是否存在class2,如果必要则切换为class1。如果当前没有任何类,则我们不执行任何操作。

在使用插件的代码中,我们将click处理程序绑定到表格上,在单击表格时对每一行调用.swapClass()。我们希望这将把标题行的类从one更改为two,并将总和行的类从two更改为one

然而,我们观察到了不同的结果:

每一行都收到了two类。要解决这个问题,我们需要正确处理具有多个选定元素的 jQuery 对象。

隐式迭代

我们需要记住,jQuery 选择器表达式总是可以匹配零个、一个或多个元素。在设计插件方法时,我们必须考虑到这些情况中的任何一种。在这种情况下,我们正在调用.hasClass(),它仅检查第一个匹配的元素。相反,我们需要独立地检查每个元素并对其采取行动。

无论匹配的元素数量如何,保证正确行为的最简单方法是始终在方法上下文中调用.each();这强制执行隐式迭代,这对于保持插件和内置方法之间的一致性至关重要。在.each()回调函数中,第二个参数依次引用每个 DOM 元素,因此我们可以调整我们的代码来分别测试和应用类到每个匹配的元素:

(function($) {
  $.fn.swapClass = function(class1, class2) {
    this
      .each((i, element) => {
        const $element = $(element);

        if ($element.hasClass(class1)) {
          $element
            .removeClass(class1)
            .addClass(class2);
        } else if ($element.hasClass(class2)) {
          $element
            .removeClass(class2)
            .addClass(class1);
        }
      });
  };
})(jQuery); 

列表 8.9

现在,当我们点击表格时,切换类而不影响没有应用任何类的行:

启用方法链

除了隐式迭代之外,jQuery 用户还应该能够依赖链接行为。这意味着我们需要从所有插件方法中返回一个 jQuery 对象,除非该方法明确用于检索不同的信息片段。返回的 jQuery 对象通常只是作为this提供的一个。如果我们使用.each()来迭代this,我们可以直接返回其结果:

(function($) {
  $.fn.swapClass = function(class1, class2) {
    return this
      .each((i, element) => {
        const $element = $(element);

        if ($element.hasClass(class1)) {
          $element
            .removeClass(class1)
            .addClass(class2);
        } else if ($element.hasClass(class2)) {
          $element
            .removeClass(class2)
            .addClass(class1);
        }
      });
  };
})(jQuery); 

列表 8.10

之前,当我们调用.swapClass()时,我们必须开始一个新语句来处理元素。然而,有了return语句,我们可以自由地将我们的插件方法与内置方法链接起来。

提供灵活的方法参数

在第七章 使用插件 中,我们看到了一些插件,可以通过参数进行微调,以达到我们想要的效果。我们看到,一个构造巧妙的插件通过提供合理的默认值来帮助我们,这些默认值可以被独立地覆盖。当我们制作自己的插件时,我们应该以用户为重心来遵循这个例子。

为了探索各种方法,让插件的用户自定义其行为,我们需要一个具有多个可以进行调整和修改的设置的示例。作为我们的示例,我们将通过使用更为武断的 JavaScript 方法来复制 CSS 的一个特性--这种方法更适合于演示而不是生产代码。我们的插件将通过在页面上不同位置叠加部分透明的多个副本来模拟元素上的阴影:

(function($) {
  $.fn.shadow = function() {
    return this.each((i, element) => {
      const $originalElement = $(element);

      for (let i = 0; i < 5; i++) {
        $originalElement
          .clone()
          .css({
            position: 'absolute',
            left: $originalElement.offset().left + i,
            top: $originalElement.offset().top + i,
            margin: 0,
            zIndex: -1,
            opacity: 0.1
          })
          .appendTo('body');
      }
    });
  };
})(jQuery); 

代码清单 8.11

对于每个调用此方法的元素,我们会制作多个元素的克隆,并调整它们的不透明度。这些克隆元素被绝对定位在原始元素的不同偏移量处。目前,我们的插件不接受参数,因此调用该方法很简单:

$(() => { 
  $('h1').shadow(); 
}); 

此方法调用会在标题文本上产生一个非常简单的阴影效果:

接下来,我们可以为插件方法引入一些灵活性。该方法的操作依赖于用户可能希望修改的几个数值。我们可以将它们转换为参数,以便根据需要进行更改。

选项对象

我们在 jQuery API 中看到了许多示例,其中options对象被提供为方法的参数,例如.animate()$.ajax()。这可以是向插件用户公开选项的更友好的方式,而不是我们刚刚在.swapClass()插件中使用的简单参数列表。对象文字为每个参数提供了可视标签,并且使参数的顺序变得无关紧要。此外,每当我们可以在我们的插件中模仿 jQuery API 时,我们都应该这样做。这将增加一致性,从而提高易用性:

(($) => {
  $.fn.shadow = function(options) {
    return this.each((i, element) => {
      const $originalElement = $(element);

      for (let i = 0; i < options.copies; i++) {
        $originalElement
          .clone()
          .css({
            position: 'absolute',
            left: $originalElement.offset().left + i,
            top: $originalElement.offset().top + i,
            margin: 0,
            zIndex: -1,
            opacity: options.opacity
          })
          .appendTo('body');
      }
    });
  };
})(jQuery);

代码清单 8.12

现在可以自定义制作的副本数量及其不透明度。在我们的插件中,每个值都作为函数的options参数的属性访问。

现在调用此方法需要我们提供包含选项值的对象:

$(() => {
  $('h1')
    .shadow({ 
      copies: 3, 
      opacity: 0.25 
    }); 
}); 

可配置性是一种改进,但现在我们必须每次都提供两个选项。接下来,我们将看看如何允许我们的插件用户省略任一选项。

默认参数值

随着方法的参数数量增加,我们不太可能总是想要指定每个参数。合理的默认值集合可以使插件接口更加易用。幸运的是,使用对象传递参数可以帮助我们完成这项任务;简单地省略对象中的任何项并用默认值替换它是很简单的:

(($) => {
  $.fn.shadow = function(opts) {
    const defaults = {
      copies: 5,
      opacity: 0.1
    };
    const options = $.extend({}, defaults, opts); 

    // ... 
  }; 
})(jQuery); 

代码清单 8.13

在这里,我们定义了一个名为defaults的新对象。 实用函数$.extend()允许我们使用提供的opts对象作为参数,并使用defaults在必要时创建一个新的options对象。 extend()函数将传递给它的任何对象合并到第一个参数中。 这就是为什么我们将空对象作为第一个参数传递的原因,以便我们为选项创建一个新对象,而不是意外地销毁现有数据。 例如,如果默认值在代码的其他位置定义,并且我们意外地替换了其值呢?

我们仍然使用对象字面量调用我们的方法,但现在我们只能指定需要与其默认值不同的参数:

$(() => { 
  $('h1')
    .shadow({ 
      copies: 3 
    }); 
}); 

未指定的参数使用其默认值。 $.extend()方法甚至接受 null 值,因此如果默认参数都可接受,则我们的方法可以在不产生 JavaScript 错误的情况下调用:

$(() => { 
  $('h1').shadow(); 
}); 

回调函数

当然,有些方法参数可能比简单的数字值更复杂。 我们在整个 jQuery API 中经常看到的一种常见参数类型是回调函数。 回调函数可以为插件提供灵活性,而无需在创建插件时进行大量准备。

要在我们的方法中使用回调函数,我们只需将函数对象作为参数接受,并在我们的方法实现中适当地调用该函数。 例如,我们可以扩展我们的文本阴影方法,以允许用户自定义阴影相对于文本的位置:

(($) => {
  $.fn.shadow = function(opts) {
    const defaults = {
      copies: 5,
      opacity: 0.1,
      copyOffset: index => ({
        x: index,
        y: index
      })
    };
    const options = $.extend({}, defaults, opts);

    return this.each((i, element) => {
      const $originalElement = $(element);

      for (let i = 0; i < options.copies; i++) {
        const offset = options.copyOffset(i);

        $originalElement
          .clone()
          .css({
            position: 'absolute',
            left: $originalElement.offset().left + offset.x,
            top: $originalElement.offset().top + offset.y,
            margin: 0,
            zIndex: -1,
            opacity: options.opacity
          })
          .appendTo('body');
      }
    });
  };
})(jQuery);

列表 8.14

阴影的每个片段与原始文本的偏移量不同。 以前,此偏移量仅等于副本的索引。 但是,现在,我们正在使用copyOffset()函数计算偏移量,该函数是用户可以覆盖的选项。 因此,例如,我们可以为两个维度的偏移提供负值:

$(() => { 
  $('h1').shadow({ 
    copyOffset: index => ({
      x: -index,
      y: -2 * index
    }) 
  }); 
}); 

这将导致阴影向左上方投射,而不是向右下方:

回调函数允许简单修改阴影的方向,或者如果插件用户提供了适当的回调,则允许更复杂的定位。 如果未指定回调,则再次使用默认行为。

可定制的默认值

通过为我们的方法参数提供合理的默认值,我们可以改善使用插件的体验,正如我们所见。 但是,有时很难预测什么是合理的默认值。 如果脚本作者需要多次调用我们的插件,并且需要不同于我们设置的默认值的参数集,那么自定义这些默认值的能力可能会显着减少需要编写的代码量。

要使默认值可定制,我们需要将它们从我们的方法定义中移出,并放入可由外部代码访问的位置:

(() => { 
  $.fn.shadow = function(opts) { 
    const options = $.extend({}, $.fn.shadow.defaults, opts); 
    // ... 
  }; 

  $.fn.shadow.defaults = { 
    copies: 5, 
    opacity: 0.1, 
    copyOffset: index => ({
      x: index,
      y: index
    }) 
  }; 
})(jQuery); 

列表 8.15

默认值现在在阴影插件的命名空间中,并且可以直接使用 $.fn.shadow.defaults 引用。现在,使用我们的插件的代码可以更改所有后续对 .shadow() 的调用所使用的默认值。选项也仍然可以在调用方法时提供:

$(() => { 
  $.fn.shadow.defaults.copies = 10;
  $('h1')
    .shadow({
      copyOffset: index => ({
        x: -index,
        y: index
    })
  });
}); 

这个脚本将使用 10 个元素的副本创建一个阴影,因为这是新的默认值,但也会通过提供的 copyOffset 回调将阴影投射到左侧和向下:

使用 jQuery UI 小部件工厂创建插件。

正如我们在第七章中看到的,使用插件,jQuery UI 有各种各样的小部件--呈现特定类型的 UI 元素的插件,如按钮或滑块。这些小部件向 JavaScript 程序员提供一致的 API。这种一致性使得学习使用其中一个变得容易。当我们编写的插件将创建一个新的用户界面元素时,通过使用小部件插件扩展 jQuery UI 库通常是正确的选择。

小部件是一段复杂的功能,但幸运的是我们不需要自己创建。jQuery UI 核心包含一个名为 $.widget()factory 方法,它为我们做了很多工作。使用这个工厂将有助于确保我们的代码符合所有 jQuery UI 小部件共享的 API 标准。

使用小部件工厂创建的插件具有许多不错的功能。我们只需很少的努力就能得到所有这些好处(以及更多):

  • 插件变得 有状态,这意味着我们可以在应用插件后检查、修改或甚至完全撤销插件的效果。

  • 用户提供的选项会自动与可定制的默认选项合并。

  • 多个插件方法被无缝地合并为单个 jQuery 方法,接受一个字符串来标识调用哪个子方法。

  • 插件触发的自定义事件处理程序可以访问小部件实例的数据。

实际上,这些优势非常好,以至于我们可能希望使用小部件工厂来构建任何合适复杂的插件,无论是 UI 相关的还是其他的。

创建一个小部件。

以我们的示例为例,我们将制作一个插件,为元素添加自定义工具提示。一个简单的工具提示实现会为页面上每个要显示工具提示的元素创建一个 <div> 容器,并在鼠标光标悬停在目标上时将该容器定位在元素旁边。

jQuery UI 库包含其自己内置的高级工具提示小部件,比我们将在这里开发的更为先进。我们的新小部件将覆盖内置的 .tooltip() 方法,这不是我们在实际项目中可能做的事情,但它将允许我们演示几个重要的概念而不会增加不必要的复杂性。

每次调用$.widget()时,小部件工厂都会创建一个 jQuery UI 插件。此函数接受小部件的名称和包含小部件属性的对象。小部件的名称必须被命名空间化;我们将使用命名空间ljq和插件名称tooltip。因此,我们的插件将通过在 jQuery 对象上调用.tooltip()来调用。

第一个小部件属性我们将定义为._create()

(($) => {
  $.widget('ljq.tooltip', {
    _create() {
      this._tooltipDiv = $('<div/>')
        .addClass([
          'ljq-tooltip-text',
          'ui-widget',
          'ui-state-highlight',
          'ui-corner-all'
        ].join(' '))
        .hide()
        .appendTo('body');
      this.element
        .addClass('ljq-tooltip-trigger')
        .on('mouseenter.ljq-tooltip', () => { this._open(); })
        .on('mouseleave.ljq-tooltip', () => { this._close(); });
    }
  });
})(jQuery); 

列表 8.16

此属性是一个函数,当调用.tooltip()时,小部件工厂将每匹配一个元素在 jQuery 对象中调用一次。

小部件属性,如_create,以下划线开头,被认为是私有的。我们稍后将讨论公共函数。

在这个创建函数内部,我们设置了我们的提示以便未来显示。为此,我们创建了新的<div>元素并将其添加到文档中。我们将创建的元素存储在this._tooltipDiv中以备后用。

在我们的函数上下文中,this指的是当前小部件实例,我们可以向该对象添加任何属性。该对象还具有一些内置属性,对我们也很方便;特别是,this.element给了我们一个指向最初选定的元素的 jQuery 对象。

我们使用this.elementmouseentermouseleave处理程序绑定到提示触发元素上。我们需要这些处理程序在鼠标开始悬停在触发器上时打开提示,并在鼠标离开时关闭它。请注意,事件名称被命名空间化为我们的插件名称。正如我们在第三章中讨论的处理事件,命名空间使我们更容易添加和删除事件处理程序,而不会影响其他代码也想要绑定处理程序到元素上。

接下来,我们需要定义绑定到mouseentermouseleave处理程序的._open()._close()方法:

(() => { 
  $.widget('ljq.tooltip', { 
    _create() { 
      // ... 
    }, 

    _open() {
      const elementOffset = this.element.offset();
      this._tooltipDiv
        .css({
          position: 'absolute',
          left: elementOffset.left,
          top: elementOffset.top + this.element.height()
        })
        .text(this.element.data('tooltip-text'))
        .show();
    },

    _close() { 
      this._tooltipDiv.hide(); 
    } 
  }); 
})(jQuery); 

列表 8.17

._open()._close()方法本身是不言自明的。这些不是特殊名称,而是说明我们可以在我们的小部件中创建任何私有函数,只要它们的名称以下划线开头。当提示被打开时,我们用 CSS 定位它并显示它;当它关闭时,我们只需隐藏它。

在打开过程中,我们需要填充提示信息。我们使用.data()方法来做到这一点,它可以获取和设置与任何元素关联的任意数据。在这种情况下,我们使用该方法来获取每个元素的data-tooltip-text属性的值。

有了我们的插件,代码$('a').tooltip()将导致鼠标悬停在任何锚点上时显示提示:

到目前为止,插件并不是很长,但是密集地包含了复杂的概念。为了让这种复杂性发挥作用,我们可以做的第一件事就是使我们的小部件具有状态。小部件的状态将允许用户根据需要启用和禁用它,甚至在创建后完全销毁它。

销毁小部件

我们已经看到,小部件工厂创建了一个新的 jQuery 方法,在我们的案例中称为 .tooltip(),可以不带参数调用以将小部件应用于一组元素。不过,这个方法还可以做更多的事情。当我们给这个方法一个字符串参数时,它会调用相应名称的方法。

内置方法之一称为 destroy。调用 .tooltip('destroy') 将从页面中删除提示小部件。小部件工厂会完成大部分工作,但如果我们在 ._create() 中修改了文档的某些部分(正如我们在这里所做的,通过创建提示文本 <div>),我们需要自己清理:

(($) => {
  $.widget('ljq.tooltip', { 
    _create() { 
      // ... 
    }, 

    destroy() {
      this._tooltipDiv.remove();
      this.element
        .removeClass('ljq-tooltip-trigger')
        .off('.ljq-tooltip');
      this._superApply(arguments);
    },

    _open() { 
      // ... 
    }, 

    _close() { 
      // ... 
    } 
  }); 
})(jQuery); 

列表 8.18

这段新代码被添加为小部件的一个新属性。该函数撤销了我们所做的修改,然后调用原型的 destroy 版本,以便自动清理发生。 _super()_superApply() 方法调用了同名的基础小部件方法。这样做总是一个好主意,这样基础小部件中的适当初始化操作就会执行。

注意 destroy 前面没有下划线;这是一个 public 方法,我们可以用 .tooltip('destroy') 调用它。

启用和禁用小部件

除了完全销毁之外,任何小部件都可以被暂时禁用,稍后重新启用。基础小部件方法 enabledisable 通过将 this.options.disabled 的值设置为 truefalse 来帮助我们。我们所要做的就是在我们的小部件采取任何行动之前检查这个值:

_open() {
  if (this.options.disabled) {
    return;
  }

  const elementOffset = this.element.offset();
  this._tooltipDiv
    .css({
      position: 'absolute',
      left: elementOffset.left,
      top: elementOffset.top + this.element.height()
    })
    .text(this.element.data('tooltip-text'))
    .show();
}

列表 8.19

在这个额外的检查放置后,一旦调用 .tooltip('disable'),提示就停止显示,并且在调用 .tooltip('enable') 之后再次显示。

接受小部件选项

现在是时候使我们的小部件可定制了。就像我们在构建 .shadow() 插件时看到的那样,为小部件提供一组可定制的默认值并用用户指定的选项覆盖这些默认值是友好的。几乎所有这个过程中的工作都是由小部件工厂完成的。我们所需要做的就是提供一个 options 属性:

options: { 
  offsetX: 10, 
  offsetY: 10, 
  content: element => $(element).data('tooltip-text') 
}, 

列表 8.20

options 属性是一个普通对象。我们的小部件的所有有效选项都应该被表示出来,这样用户就不需要提供任何强制性的选项。在这里,我们为提示相对于其触发元素的 x 和 y 坐标提供了一个函数,以及一个为每个元素生成提示文本的函数。

我们代码中唯一需要检查这些选项的部分是 ._open()

_open() {
  if (this.options.disabled) {
    return;
  }

  const elementOffset = this.element.offset();
  this._tooltipDiv
    .css({
      position: 'absolute',
      left: elementOffset.left + this.options.offsetX,
      top:
        elementOffset.top +
        this.element.height() +
        this.options.offsetY
    })
    .text(this.options.content(this.element))
    .show();
} 

列表 8.21

_open方法内部,我们可以使用this.options访问这些属性。通过这种方式,我们总是能够得到选项的正确值:默认值或者用户提供的覆盖值。

我们仍然可以像.tooltip()这样无参数地添加我们的小部件,并获得默认行为。现在我们可以提供覆盖默认行为的选项:.tooltip({ offsetX: -10, offsetX: 25 })。小部件工厂甚至让我们在小部件实例化后更改这些选项:.tooltip('option', 'offsetX', 20)。下次访问选项时,我们将看到新值。

对选项更改做出反应

如果我们需要立即对选项更改做出反应,我们可以在小部件中添加一个_setOption函数来处理更改,然后调用_setOption的默认实现。

添加方法

内置方法很方便,但通常我们希望向插件的用户公开更多的钩子,就像我们使用内置的destroy方法所做的那样。我们已经看到如何在小部件内部创建新的私有函数。创建公共方法也是一样的,只是小部件属性的名称不以下划线开头。我们可以利用这一点很简单地创建手动打开和关闭工具提示的方法:

open() { 
  this._open(); 
},
close() { 
  this._close(); 
}

列表 8.22

就是这样!通过添加调用私有函数的公共方法,我们现在可以使用.tooltip('open')打开工具提示,并使用.tooltip('close')关闭它。小部件工厂甚至会为我们处理一些细节,比如确保链式调用继续工作,即使我们的方法不返回任何东西。

触发小部件事件

一个优秀的插件不仅扩展了 jQuery,而且还为其他代码提供了许多扩展插件本身的机会。提供这种可扩展性的一个简单方法是支持与插件相关的一组自定义事件。小部件工厂使这个过程变得简单:

_open() {
  if (this.options.disabled) {
    return;
  }

  const elementOffset = this.element.offset();
  this._tooltipDiv
    .css({
      position: 'absolute',
      left: elementOffset.left + this.options.offsetX,
      top:
        elementOffset.top +
        this.element.height() +
        this.options.offsetY
    })
    .text(this.options.content(this.element))
    .show();
  this._trigger('open');
},

_close: function() { 
  this._tooltipDiv.hide(); 
  this._trigger('close'); 
} 

列表 8.23

在我们的函数中调用this._trigger()允许代码监听新的自定义事件。事件的名称将以我们的小部件名称为前缀,因此我们不必过多担心与其他事件的冲突。例如,在我们的工具提示打开函数中调用this._trigger('open'),每次工具提示打开时都会发出名为tooltipopen的事件。我们可以通过在元素上调用.on('tooltipopen')来监听此事件。

这只是揭示了一个完整的小部件插件可能具有的潜力,但给了我们构建一个具有 jQuery UI 用户所期望的功能和符合标准的小部件所需的工具。

插件设计建议

现在,我们已经研究了通过创建插件来扩展 jQuery 和 jQuery UI 的常见方式,我们可以回顾并补充我们学到的内容,列出一些建议:

  • 通过使用jQuery或将$传递给 IIFE 来保护$别名免受其他库的潜在干扰,以便它可以用作局部变量。

  • 无论是扩展 jQuery 对象与 $.myPlugin 还是扩展 jQuery 原型与 $.fn.myPlugin,都不要向 $ 命名空间添加超过一个属性。额外的公共方法和属性应添加到插件的命名空间中(例如,$.myPlugin.publicMethod$.fn.myPlugin.pluginProperty)。

  • 提供包含插件默认选项的对象:$.fn.myPlugin.defaults = {size: 'large'}

  • 允许插件用户选择性地覆盖所有后续调用方法的默认设置($.fn.myPlugin.defaults.size = 'medium';)或单个调用的默认设置($('div').myPlugin({size: 'small'});)。

  • 在大多数情况下,当扩展 jQuery 原型时($.fn.myPlugin),返回 this 以允许插件用户将其他 jQuery 方法链接到它(例如,$('div').myPlugin().find('p').addClass('foo'))。

  • 当扩展 jQuery 原型时($.fn.myPlugin),通过调用 this.each() 强制隐式迭代。

  • 在适当的情况下使用回调函数,以允许灵活修改插件的行为,而无需更改插件的代码。

  • 如果插件需要用户界面元素或需要跟踪元素状态,请使用 jQuery UI 小部件工厂创建。

  • 使用像 QUnit 这样的测试框架为插件维护一组自动化单元测试,以确保其按预期工作。有关 QUnit 的更多信息,请参见附录 A。

  • 使用诸如 Git 等版本控制系统跟踪代码的修订。考虑在 GitHub(github.com/)上公开托管插件,并允许其他人贡献。

  • 如果要使插件可供他人使用,请明确许可条款。考虑使用 MIT 许可证,jQuery 也使用此许可证。

分发插件

遵循前述建议,我们可以制作出符合经过时间考验的传统的干净、可维护的插件。如果它执行一个有用的、可重复使用的任务,我们可能希望与 jQuery 社区分享。

除了按照早前定义的方式正确准备插件代码之外,我们还应该在分发之前充分记录插件的操作。我们可以选择适合我们风格的文档格式,但可能要考虑一种标准,比如 JSDoc(在 usejsdoc.org/ 中描述)。有几种自动文档生成器可用,包括 docco(jashkenas.github.com/docco/)和 dox(github.com/visionmedia/dox)。无论格式如何,我们都必须确保我们的文档涵盖了插件方法可用的每个参数和选项。

插件代码和文档可以托管在任何地方;npm(www.npmjs.com/)是标准选项。有关将 jQuery 插件发布为 npm 软件包的更多信息,请查看此页面:blog.npmjs.org/post/112064849860/using-jquery-plugins-with-npm

摘要

在本章中,我们看到 jQuery 核心提供的功能不必限制库的功能。除了我们在第七章使用插件中探讨的现成插件外,我们现在知道如何自己扩展功能菜单。

我们创建的插件包含各种功能,包括使用 jQuery 库的全局函数、用于操作 DOM 元素的 jQuery 对象的新方法以及复杂的 jQuery UI 小部件。有了这些工具,我们可以塑造 jQuery 和我们自己的 JavaScript 代码,使其成为我们想要的任何形式。

练习

挑战练习可能需要使用api.jquery.com/上的官方 jQuery 文档。

  1. 创建名为.slideFadeIn().slideFadeOut()的新插件方法,将.fadeIn().fadeOut()的不透明度动画与.slideDown().slideUp()的高度动画结合起来。

  2. 扩展.shadow()方法的可定制性,以便插件用户可以指定克隆副本的 z-index。

  3. 为工具提示小部件添加一个名为isOpen的新子方法。该子方法应该在工具提示当前显示时返回true,否则返回false

  4. 添加监听我们小部件触发的tooltipopen事件的代码,并在控制台中记录一条消息。

  5. 挑战:为工具提示小部件提供一个替代的content选项,该选项通过 Ajax 获取锚点的href指向页面的内容,并将该内容显示为工具提示文本。

  6. 挑战:为工具提示小部件提供一个新的effect选项,如果指定了,将应用指定的 jQuery UI 效果(比如explode)来显示和隐藏工具提示。

第九章:高级选择器和遍历

2009 年 1 月,jQuery 的创始人约翰·雷西格(John Resig)推出了一个名为Sizzle的新开源 JavaScript 项目。作为一个独立的CSS 选择器引擎,Sizzle 的编写旨在让任何 JavaScript 库都能够在几乎不修改其代码库的情况下采用它。事实上,jQuery 自从 1.3 版本以来一直在使用 Sizzle 作为其自己的选择器引擎。

Sizzle 是 jQuery 中负责解析我们放入$()函数中的 CSS 选择器表达式的组件。它确定要使用哪些原生 DOM 方法,因为它构建了一个我们可以用其他 jQuery 方法操作的元素集合。Sizzle 和 jQuery 的遍历方法集合的结合使得 jQuery 成为查找页面元素的非常强大的工具。

在第二章,选择元素中,我们查看了 jQuery 库中每种基本类型的选择器和遍历方法,以便我们了解在 jQuery 库中可用的内容。在这个更高级的章节中,我们将涵盖:

  • 使用选择器以各种方式查找和过滤数据

  • 编写添加新选择器和 DOM 遍历方法的插件

  • 优化我们的选择器表达式以获得更好的性能

  • 了解 Sizzle 引擎的一些内部工作 ings

选择和遍历重访

为了更深入地了解选择器和遍历,我们将构建一个脚本,提供更多选择和遍历示例以进行检查。对于我们的示例,我们将构建一个包含新闻项列表的 HTML 文档。我们将这些项目放在一个表格中,以便我们可以以几种方式选择行和列进行实验:

<div id="topics"> 
  Topics: 
  <a href="topics/all.html" class="selected">All</a> 
  <a href="topics/community.html">Community</a> 
  <a href="topics/conferences.html">Conferences</a> 
  <!-- continued... --> 
</div> 
<table id="news"> 
  <thead> 
    <tr> 
      <th>Date</th> 
      <th>Headline</th> 
      <th>Author</th> 
      <th>Topic</th> 
    </tr> 
  </thead> 
  <tbody> 
    <tr> 
      <th colspan="4">2011</th> 
    </tr> 
    <tr> 
      <td>Apr 15</td> 
      <td>jQuery 1.6 Beta 1 Released</td> 
      <td>John Resig</td> 
      <td>Releases</td> 
    </tr> 
    <tr> 
      <td>Feb 24</td> 
      <td>jQuery Conference 2011: San Francisco Bay Area</td> 
      <td>Ralph Whitbeck</td> 
      <td>Conferences</td> 
    </tr> 
    <!-- continued... --> 
  </tbody> 
</table> 

获取示例代码

您可以从以下 GitHub 存储库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

从这个代码片段中,我们可以看到文档的结构。表格有四列,代表日期、标题、作者和主题,但是一些表格行包含一个日历年的副标题,而不是这四个项目:

在标题和表格之间,有一组链接,代表着表格中的每个新闻主题。对于我们的第一个任务,我们将更改这些链接的行为,以原地过滤表格,而不需要导航到不同的页面。

动态表格过滤

为了使用主题链接来过滤表格,我们需要阻止其默认的链接行为。我们还应该为当前选择的主题给用户提供一些反馈:

$(() => {
  $('#topics a')
    .click((e) => {
      e.preventDefault();
      $(e.target)
        .addClass('selected')
        .siblings('.selected')
        .removeClass('selected');
    });
}); 

列表 9.1

当点击其中一个链接时,我们会从所有主题链接中删除selected类,然后将selected类添加到新主题上。调用.preventDefault()可以阻止链接被跟踪。

接下来,我们需要实际执行过滤操作。作为解决此问题的第一步,我们可以隐藏表格中不包含主题文本的每一行:

$(() => {
  $('#topics a')
    .click((e) => {
      e.preventDefault();
      const topic = $(e.target).text();

      $(e.target)
        .addClass('selected')
        .siblings('.selected')
        .removeClass('selected');

      $('#news tr').show();
      if (topic != 'All') {
        $(`#news tr:has(td):not(:contains("${topic}"))`)
          .hide();
      }
    });
}); 

列表 9.2

现在我们将链接的文本存储在常量topic中,以便我们可以将其与表格中的文本进行比较。首先,我们显示所有的表行,然后,如果主题不是全部,我们就隐藏不相关的行。我们用于此过程的选择器有点复杂:

#news tr:has(td):not(:contains("topic")) 

选择器从简单开始,使用#news tr定位表中的所有行。然后我们使用:has()自定义选择器来过滤这个元素集。这个选择器将当前选定的元素减少到那些包含指定后代的元素。在这种情况下,我们正在消除要考虑的标题行(如日历年份),因为它们不包含<td>单元格。

一旦我们找到了表的行,其中包含实际内容,我们就需要找出哪些行与所选主题相关。:contains()自定义选择器仅匹配具有给定文本字符串的元素;将其包装在:not()选择器中,然后我们就可以隐藏所有不包含主题字符串的行。

这段代码运行得足够好,除非主题恰好出现在新闻标题中,例如。我们还需要处理一个主题是另一个主题子串的可能性。为了处理这些情况,我们需要对每一行执行代码:

$(() => {
  $('#topics a')
    .click((e) => {
      e.preventDefault();
      const topic = $(e.target).text();

      $(e.target)
        .addClass('selected')
        .siblings('.selected')
        .removeClass('selected');

      $('#news tr').show();
      if (topic != 'All') {
        $('#news')
          .find('tr:has(td)')
          .not((i, element) =>
            $(element)
              .children(':nth-child(4)')
              .text() == topic
          )
          .hide();
      }
    });
}); 

列表 9.3

这段新代码通过添加 DOM 遍历方法消除了一些复杂的选择器表达式文本。.find()方法的作用就像之前将#newstr分开的空格一样,但是.not()方法做了:not()不能做的事情。就像我们在第二章中看到的.filter()方法一样,.not()可以接受一个回调函数,每次测试一个元素时调用。如果该函数返回true,则将该元素从结果集中排除。

选择器与遍历方法

使用选择器或其等效的遍历方法的选择在性能上也有影响。我们将在本章后面更详细地探讨这个选择。

.not()方法的过滤函数中,我们检查行的子元素,找到第四个(也就是Topic列中的单元格)。对这个单元格的文本进行简单检查就能告诉我们是否应该隐藏该行。只有匹配的行会被显示:

条纹表行

在第二章中,我们的选择器示例之一演示了如何将交替的行颜色应用于表格。我们看到,:even:odd自定义选择器可以轻松完成这项任务,CSS 本地的:nth-child()伪类也可以完成:

$(() => { 
  $('#news tr:nth-child(even)')
    .addClass('alt'); 
}); 

列表 9.4

这个直接的选择器找到每个表行,因为每年的新闻文章都放在自己的<tbody>元素中,所以每个部分都重新开始交替。

对于更复杂的行条纹挑战,我们可以尝试一次给两行设置alt类。前两行将收到类,然后接下来的两行将不会,以此类推。为了实现这一点,我们需要重新审视过滤函数

$(() => { 
  $('#news tr')
    .filter(i => (i % 4) < 2)
    .addClass('alt'); 
}); 

列表 9.5

在第二章中的我们的.filter()示例中,选择元素,以及列表 9.3中的.not()示例中,我们的过滤函数会检查每个元素,以确定是否将其包含在结果集中。但是,在这里,我们不需要关于元素的信息来确定是否应该包含它。相反,我们需要知道它在原始元素集合中的位置。这些信息作为参数传递给函数,并且我们将其称为i

现在,i参数保存了元素的从零开始的索引。有了这个,我们可以使用取模运算符(%)来确定我们是否在应该接收alt类的一对元素中。现在,我们在整个表中有两行间隔。

然而,还有一些松散的地方需要清理。因为我们不再使用:nth-child()伪类,所以交替不再在每个<tbody>中重新开始。另外,我们应该跳过表头行以保持一致的外观。通过进行一些小的修改,可以实现这些目标:

$(() => {
  $('#news tbody')
    .each((i, element) => {
      $(element)
        .children()
        .has('td')
        .filter(i => (i % 4) < 2)
        .addClass('alt');
    });
}); 

列表 9.6

为了独立处理每组行,我们可以使用.each()调用对<tbody>元素进行循环。在循环内部,我们像在列表 9.3中那样排除子标题行,使用.has()。结果是表被分成两行的一组进行条纹处理:

结合过滤和条纹

我们的高级表格条纹现在工作得很好,但在使用主题过滤器时行为奇怪。为了使这两个函数协调良好,我们需要在每次使用过滤器时重新为表添加条纹。我们还需要考虑当前行是否隐藏,以确定在哪里应用alt类:

$(() => {
  function stripe() {
    $('#news')
      .find('tr.alt')
      .removeClass('alt')
      .end()
      .find('tbody')
      .each((i, element) => {
        $(element)
          .children(':visible')
          .has('td')
          .filter(i => (i % 4) < 2)
          .addClass('alt');
      });
  }
  stripe();

  $('#topics a')
    .click((e) => {
      e.preventDefault();
      const topic = $(e.target).text();

      $(e.target)
        .addClass('selected')
        .siblings('.selected')
        .removeClass('selected');

      $('#news tr').show();
      if (topic != 'All') {
        $('#news')
          .find('tr:has(td)')
          .not((i, element) =>
            $(element)
              .children(':nth-child(4)')
              .text() == topic
          )
          .hide();
      }

      stripe();
    });
}); 

列表 9.7

列表 9.3中的过滤代码与我们的行条纹例程结合起来,这个脚本现在定义了一个名为stripe()的函数,当文档加载时调用一次,每当点击主题链接时再次调用。在函数内部,我们负责从不再需要它的行中删除alt类,以及将所选行限制为当前显示的行。我们使用:visible伪类来实现这一点,它(以及它的对应项:hidden)尊重元素是否由于各种原因而隐藏,包括具有display值为none,或widthheight值为0

我们现在可以过滤我们表的行而保留我们的行条纹:

更多选择器和遍历方法

即使在我们看到的所有示例之后,我们也没有接近探索使用 jQuery 在页面上找到元素的每一种方式。我们有数十个选择器和 DOM 遍历方法可用,并且每个方法都有特定的实用性,我们可能需要调用其中的某一个。

要找到适合我们需求的选择器或方法,我们有许多资源可用。本书末尾的快速参考列出了每个选择器和方法,并简要描述了每个选择器和方法。然而,对于更详细的描述和用法示例,我们需要更全面的指南,比如在线 jQuery API 参考。该网站列出了所有选择器在 api.jquery.com/category/selectors/,以及遍历方法在 api.jquery.com/category/traversing/

自定义和优化选择器

我们看到的许多技术都为我们提供了一个工具箱,可用于找到我们想要处理的任何页面元素。然而,故事并没有结束;有很多关于如何有效执行我们的元素查找任务的知识需要学习。这种效率可以以编写和阅读更简单的代码,以及在 web 浏览器内更快执行的代码形式呈现。

编写自定义选择器插件

提高可读性的一种方法是将代码片段封装在可重用组件中。我们通过创建函数一直在做这件事。在 第八章,开发插件 中,我们通过创建 jQuery 插件来为 jQuery 对象添加方法来扩展这个想法。然而,插件不仅仅可以帮助我们重用代码。插件还可以提供额外的选择器表达式,比如 Cycle 在 第七章,使用插件 中给我们的 :paused 选择器。

要添加的最简单类型的选择器表达式是伪类。这是以冒号开头的表达式,比如 :checked:nth-child()。为了说明创建选择器表达式的过程,我们将构建一个名为 :group() 的伪类。这个新选择器将封装我们用来找到表格行以执行条纹化的代码,就像 列表 9.6 中一样。

当使用选择器表达式查找元素时,jQuery 会在内部对象 expr 中查找指令。这个对象中的值的行为类似于我们传递给 .filter().not() 的过滤函数,包含导致每个元素包含在结果集中的 JavaScript 代码,仅当函数返回 true 时才会包含。我们可以使用 $.extend() 函数向这个对象添加新的表达式:

(($) => {
  $.extend($.expr[':'], {
    group(element, index, matches) {
      const num = parseInt(matches[3], 10);

      return Number.isInteger(num) &&
        ($(element).index() - 1) % (num * 2) < num;
    }
  });
})(jQuery); 

列表 9.8

这段代码告诉 jQuery group 是一个有效的字符串,可以跟在选择器表达式的冒号后面,当遇到它时,应调用给定的函数来确定是否应将元素包含在结果集中。

这里评估的函数传递了四个参数:

  • element:要考虑的 DOM 元素。大多数选择器都需要这个,但我们的不需要。

  • index:结果集中的 DOM 元素的索引。不幸的是,这总是 0,我们不能依赖它。这里包括它的唯一原因是因为我们需要对匹配参数进行位置访问。

  • matches:包含用于解析此选择器的正则表达式结果的数组。通常,matches[3]是数组中唯一相关的项目;在形式为:group(2)的选择器中,matches[3]项包含2,即括号内的文本。

伪类选择器可以使用这三个参数中的部分或全部信息来确定元素是否属于结果集。在这种情况下,我们只需要elementmatches。实际上,我们确实需要传递给此函数的每个元素的索引位置。由于无法依赖index参数,因此我们简单地使用.index() jQuery 方法来获取索引。

有了新的:group选择器,我们现在有了一种灵活的方式来选择交替的元素组。例如,我们可以将选择器表达式和.filter()函数从列表 9.5合并为一个单一的选择器表达式:$('#news tr:group(2)'),或者我们可以保留列表 9.7中的每节行为,并将:group()作为一个表达式在.filter()调用中使用。我们甚至可以通过简单地在括号内更改数字来更改要分组的行数:

$(() => { 
  function stripe() {
    $('#news')
      .find('tr.alt')
      .removeClass('alt')
      .end()
      .find('tbody')
      .each((i, element) => {
        $(element)
          .children(':visible')
          .has('td')
          .filter(':group(3)')
          .addClass('alt');
      });
  }

  stripe(); 
}); 

列表 9.9

现在我们可以看到,行条纹以三个一组交替:

选择器性能

在规划任何 web 开发项目时,我们需要记住创建网站所需的时间、我们可以维护代码的轻松程度和速度,以及用户与网站交互时的性能。通常,这些关注点中的前两个比第三个更重要。特别是在客户端脚本编写方面,开发者很容易陷入过早优化微优化的陷阱。这些陷阱会导致我们花费无数小时微调我们的代码,以从 JavaScript 执行时间中削减毫秒,即使一开始没有注意到性能滞后。

一个很好的经验法则是认为开发者的时间比计算机的时间更宝贵,除非用户注意到我们应用程序的速度变慢。

即使性能是一个问题,定位我们的 jQuery 代码中的瓶颈也可能很困难。正如我们在本章前面提到的,某些选择器通常比其他选择器快,将选择器的一部分移到遍历方法中可以帮助加快在页面上查找元素所需的时间。因此,选择器和遍历性能通常是开始检查我们的代码以减少用户与页面交互时可能遇到的延迟量的良好起点。

关于选择器和遍历方法的相对速度的任何判断都可能随着发布更新、更快的浏览器和新版本 jQuery 引入的聪明速度调整而过时。在性能方面,经常质疑我们的假设,并在使用像jsPerfjsperf.com)这样的工具进行测量后优化代码是个好主意。

在这种情况下,我们将检查一些简单的指南,以生成优化的 jQuery 选择器代码。

Sizzle 选择器实现

正如本章开始时所指出的,当我们将选择器表达式传递给$()函数时,jQuery 的 Sizzle 实现会解析表达式并确定如何收集其中表示的元素。在其基本形式中,Sizzle 应用最有效的本地DOM 方法,浏览器支持以获取nodeList,这是一个 DOM 元素的本机类似数组对象,jQuery 最终会将其转换为真正的数组,并将其添加到jQuery对象。以下是 jQuery 内部使用的 DOM 方法列表,以及支持它们的最新浏览器版本:

方法 选择 支持者
.getElementById() 与给定字符串匹配的唯一元素的 ID。 所有浏览器
.getElementsByTagName() 所有标签名称与给定字符串匹配的元素。 所有浏览器
.getElementsByClassName() 具有其中一个类名与给定字符串匹配的所有元素。 IE9+,Firefox 3+,Safari 4+,Chrome 4+,和 Opera 10+
.querySelectorAll() 所有匹配给定选择器表达式的元素。 IE8+,Firefox 3.5+,Safari 3+,Chrome 4+,和 Opera 10+

如果选择器表达式的某个部分不能由这些方法之一处理,Sizzle 会回退到循环遍历已经收集的每个元素,并针对表达式的每个部分进行测试。如果选择器表达式的任何部分都不能由 DOM 方法处理,Sizzle 就会以document.getElementsByTagName('*')表示的文档中所有元素的集合开始,并逐个遍历每个元素。

这种循环和测试每个元素的方法在性能上要比任何本地 DOM 方法昂贵得多。幸运的是,现代桌面浏览器的最新版本都包括本地的.querySelectorAll()方法,并且当它不能使用其他更快的本地方法时,Sizzle 会使用它--只有一个例外。当选择器表达式包含像:eq():odd:even这样没有 CSS 对应的自定义 jQuery 选择器时,Sizzle 就别无选择,只能循环和测试。

测试选择器速度

要了解 .querySelectorAll()循环测试 过程之间的性能差异,可以考虑一个文档,其中我们希望选择所有 <input type="text"> 元素。我们可以用两种方式编写选择器表达式:$('input[type="text"]'),使用 CSS 属性选择器,或者 $('input:text'),使用 自定义 jQuery 选择器。为了测试我们在这里感兴趣的选择器部分,我们将移除 input 部分,并比较 $('[type="text"]')$(':text') 的速度。JavaScript 基准测试网站 jsperf.com/ 让我们可以进行这种比较,得出戏剧性的结果。

在 jsPerf 测试中,每个测试用例会循环执行,以查看在一定时间内可以完成多少次,因此数字越高越好。在支持 .querySelectorAll() 的现代浏览器(Chrome 26、Firefox 20 和 Safari 6)中进行测试时,能够利用它的选择器比自定义的 jQuery 选择器要快得多:

图 9.1

但是,在不支持 .querySelectorAll() 的浏览器中,例如 IE 7,这两个选择器的性能几乎相同。在这种情况下,这两个选择器都会强制 jQuery 循环遍历页面上的每个元素,并分别测试每个元素:

图 9.2

当我们查看 $('input:eq(1)')$('input') .eq(1) 时,使用原生方法和不使用原生方法的选择器之间的性能差异也是显而易见的:

图 9.3

尽管每秒操作次数在不同浏览器之间有很大差异,但所有测试的浏览器在将自定义的 :eq() 选择器移出到 .eq() 方法时都显示出显著的性能提升。使用简单的 input 标签名称作为 $() 函数的参数允许快速查找,然后 .eq() 方法简单地调用数组函数来检索 jQuery 集合中的第二个元素。

作为一个经验法则,我们应尽可能使用 CSS 规范中的选择器,而不是 jQuery 的自定义选择器。但在更改选择器之前,先确认是否需要提高性能是有意义的,然后使用诸如 jsperf.com 这样的基准测试工具测试更改能够提升多少性能。

在幕后进行 DOM 遍历

在第二章中,选择元素,以及本章的开头,我们讨论了通过调用 DOM 遍历方法从一个 DOM 元素集合到另一个 DOM 元素集合的方法。我们(远非详尽)的调查包括简单到达相邻单元格的简单方法,例如 .next().parent(),以及更复杂的组合选择器表达式的方式,例如 .find().filter()。到目前为止,我们应该对这些一步步从一个 DOM 元素到另一个 DOM 元素的方法有相当牢固的掌握。

每次我们执行其中一步时,jQuery 都会记录我们的行程,留下一串面包屑,如果需要的话,我们可以按照这些面包屑回到家里。在那一章中我们简要提及的几个方法,.end().addBack(),利用了这种记录。为了能够充分利用这些方法,并且一般来说编写高效的 jQuery 代码,我们需要更多地了解 DOM 遍历方法如何执行它们的工作。

jQuery 遍历属性

我们知道,通常通过将选择器表达式传递给 $() 函数来构造 jQuery 对象实例。在生成的对象内部,存在一个包含与该选择器匹配的每个 DOM 元素引用的数组结构。不过,我们没有看到对象中隐藏的其他属性。例如,当调用 DOM 遍历方法时,.prevObject 属性保存了对调用该遍历方法的 jQuery 对象的引用。

jQuery 对象用于暴露 selectorcontext 属性。由于它们对我们没有提供任何价值,在 jQuery 3 中已经被移除。

要查看 prevObject 属性的作用,我们可以突出显示表格的任意单元格并检查其值:

$(() => { 
  const $cell = $('#release');
    .addClass('highlight'); 
  console.log('prevObject', $cell.prevObject); 
}); 

列表 9.10

此代码段将突出显示所选单个单元格,如下图所示:

我们可以看到 .prevObject 未定义,因为这是一个新创建的对象。但是,如果我们将遍历方法添加到混合中,情况就会变得更加有趣:

$(() => { 
  const $cell = $('#release')
    .nextAll()
    .addClass('highlight'); 
  console.log('prevObject', $cell.prevObject); 
}); 

列表 9.11

此更改改变了高亮显示的单元格,如下图所示:

现在,我们最初选择的单元格后面的两个单元格被突出显示。在 jQuery 对象内部,.prevObject 现在指向 .nextAll() 调用之前的原始 jQuery 对象实例。

DOM 元素栈

由于每个 jQuery 对象实例都有一个 .prevObject 属性,指向前一个对象,我们有了一个实现 的链表结构。每次遍历方法调用都会找到一组新的元素并将此集合推入堆栈。只有在我们可以对此堆栈执行某些操作时,才有用,这就是 .end().addBack() 方法发挥作用的地方。

.end() 方法简单地从堆栈的末尾弹出一个元素,这与获取 .prevObject 属性的值相同。我们在第二章中看到了一个示例,选择元素,在本章后面我们还会看到更多。然而,为了得到更有趣的例子,我们将研究 .addBack() 如何操作堆栈:

$(() => { 
  $('#release')
    .nextAll()
    .addBack()
    .addClass('highlight'); 
}); 

列表 9.12

再次,高亮显示的单元格已更改:

当调用 .addBack() 方法时,jQuery 回顾栈上的上一步并将两个元素集合合并起来。在我们的例子中,这意味着突出显示的单元格包括 .nextAll() 调用找到的两个单元格和使用选择器定位的原始单元格。然后,这个新的、合并的元素集合被推到栈上。

这种栈操作方式非常有用。为了确保在需要时这些技术能够发挥作用,每个遍历方法的实现都必须正确更新栈;这意味着如果我们想提供自己的遍历方法,我们需要了解系统的一些内部工作原理。

编写 DOM 遍历方法插件

和任何其他 jQuery 对象方法一样,遍历方法可以通过向 $.fn 添加属性来添加到 jQuery 中。我们在第八章中看到,我们定义的新的 jQuery 方法应该在匹配的元素集合上操作,然后返回 jQuery 对象,以便用户可以链式调用其他方法。当我们创建 DOM 遍历方法时,这个过程是类似的,但是我们返回的 jQuery 对象需要指向一个新的匹配元素集合。

举个例子,我们将构建一个插件,找到与给定单元格相同列的所有表格单元格。首先我们将完整地查看插件代码,然后逐个地分析它,以了解它的工作原理:

(($) => {
  $.fn.column = function() {
    var $cells = $();

    this.each(function(i, element) {
      const $td = $(element).closest('td, th');

      if ($td.length) {
        const colNum = $td[0].cellIndex + 1;
        const $columnCells = $td
          .closest('table')
          .find('td, th')
          .filter(`:nth-child(${colNum})`);

        $cells = $cells.add($columnCells);
      }
    });

    return this.pushStack($cells);
  };
})(jQuery); 

第 9.13 节

我们的 .column() 方法可以在指向零个、一个或多个 DOM 元素的 jQuery 对象上调用。为了考虑到所有这些可能性,我们使用 .each() 方法循环遍历元素,逐个将单元格列添加到变量 $cells 中。这个 $cells 变量一开始是一个空的 jQuery 对象,但随后通过 .add() 方法扩展到需要的更多 DOM 元素。

这解释了函数的外部循环;在循环内部,我们需要理解 $columnCells 如何填充表列中的 DOM 元素。首先,我们获取正在检查的表格单元格的引用。我们希望允许在表格单元格上或表格单元格内的元素上调用 .column() 方法。.closest() 方法为我们处理了这个问题;它在 DOM 树中向上移动,直到找到与我们提供的选择器匹配的元素。这个方法在事件委托中会非常有用,我们将在第十章中重新讨论,高级事件

有了我们手头的表格单元格,我们使用 DOM 的 .cellIndex 属性找到它的列号。这给了我们一个基于零的单元格列的索引;我们在稍后的一个基于一的上下文中使用它时加上 1。然后,从单元格开始,我们向上移动到最近的 <table> 元素,再返回到 <td><th> 元素,并用 :nth-child() 选择器表达式过滤这些单元格,以获取适当的列。

我们正在编写的插件仅限于简单的、非嵌套的表格,因为 .find('td, th') 调用。要支持嵌套表格,我们需要确定是否存在 <tbody> 标签,并根据适当的数量在 DOM 树中上下移动,这将增加比这个示例适当的更多复杂性。

一旦我们找到了列中的所有单元格,我们需要返回新的 jQuery 对象。我们可以从我们的方法中直接返回 $cells,但这不会正确地尊重 DOM 元素堆栈。相反,我们将 $cells 传递给 .pushStack() 方法并返回结果。该方法接受一个 DOM 元素数组,并将它们添加到堆栈中,以便后续对 .addBack().end() 等方法的调用能够正确地工作。

若要查看我们的插件运行情况,我们可以对单元格的点击做出反应,并突出显示相应的列:

$(() => { 
  $('#news td')
    .click((e) => {
      $(e.target)
        .siblings('.active')
        .removeClass('active')
        .end()
        .column()
        .addClass('active');
    });
}); 

第 9.14 节

active 类将添加到所选列,从而导致不同的着色,例如,当点击其中一位作者的姓名时:

DOM 遍历性能

关于选择器性能的经验法则同样适用于 DOM 遍历性能:在可能的情况下,我们应该优先考虑代码编写和代码维护的便利性,只有在性能是可测量的问题时才会为了优化而牺牲可读性。同样,诸如 jsperf.com/ 这样的网站有助于确定在给定多个选项的情况下采取最佳方法。

虽然应该避免过早地优化,但最小化选择器和遍历方法的重复是一个良好的实践。由于这些可能是昂贵的任务,我们做这些任务的次数越少越好。避免这种重复的两种策略是链式操作对象缓存

使用链式操作来改进性能

我们现在已经多次使用了链式操作,它使我们的代码保持简洁。链式操作也可能带来性能上的好处。

我们来自第 9.9 节stripe() 函数只定位了一次具有 ID news 的元素,而不是两次。它需要从不再需要的行中移除 alt 类,并将该类应用于新的行集。使用链式操作,我们将这两个想法合并成一个,避免了这种重复:

$(() => {
  function stripe() {
    $('#news')
      .find('tr.alt')
      .removeClass('alt')
      .end()
      .find('tbody')
      .each((i, element) => {
        $(element)
          .children(':visible')
          .has('td')
          .filter(':group(3)')
          .addClass('alt');
      });
  }

  stripe();
}); 

第 9.15 节

为了合并两次使用 $('#news'),我们再次利用了 jQuery 对象内部的 DOM 元素堆栈。第一次调用 .find() 将表行推送到堆栈上,但然后 .end() 将其从堆栈中弹出,以便下一次 .find() 调用再次操作 news 表。这种巧妙地操作堆栈的方式是避免选择器重复的便捷方式。

使用缓存来改进性能

缓存只是简单地存储操作的结果,以便可以多次使用而不必再次运行该操作。在选择器和遍历性能的背景下,我们可以将 jQuery 对象缓存到常量中以供以后使用,而不是创建一个新的对象。

回到我们的示例,我们可以重写 stripe() 函数,以避免选择器重复,而不是链接:

$(() => { 
  const $news = $('#news');

  function stripe() {
    $news
      .find('tr.alt')
      .removeClass('alt');
    $news
      .find('tbody')
      .each((i, element) => {
        $(element)
          .children(':visible')
          .has('td')
          .filter(':group(3)')
          .addClass('alt');
      });
  }

  stripe();
}); 

清单 9.16

这两个操作再次是分开的 JavaScript 语句,而不是链接在一起。尽管如此,我们仍然只执行了一次 $('#news') 选择器,通过将结果存储在 $news 中。这种缓存方法比链接更繁琐,因为我们需要单独创建存储 jQuery 对象的变量。显然,在代码中创建更多的常量比链接函数调用更不理想。但有时,链接简单地太复杂了,像这样缓存对象是更好的选择。

因为通过 ID 在页面上选择元素非常快,所以这些示例都不会对性能产生很大的影响,实际上我们会选择看起来最易读和易于维护的方法。但是当性能成为一个关注点时,这些技术是有用的工具。

总结

在本章中,我们更深入地了解了 jQuery 在查找文档中的元素方面的广泛功能。我们看了一些关于 Sizzle 选择器引擎如何工作的细节,以及这对设计有效和高效代码的影响。此外,我们还探讨了扩展和增强 jQuery 选择器和 DOM 遍历方法的方式。

进一步阅读

在本书的 附录 B、“快速参考” 中或在官方 jQuery 文档中,提供了一份完整的选择器和遍历方法列表。

练习

挑战性练习可能需要在 api.jquery.com/ 官方 jQuery 文档中使用。

  1. 修改表格行条纹的例程,使其不给第一行任何类,第二行给予 alt 类,第三行给予 alt-2 类。对每组三行的行重复此模式。

  2. 创建一个名为 :containsExactly() 的新选择器插件,它选择具有与括号内放置的内容完全匹配的文本内容的元素。

  3. 使用这个新的 :containsExactly() 选择器来重写 清单 9.3 中的过滤代码。

  4. 创建一个名为 .grandparent() 的新 DOM 遍历插件方法,它从一个或多个元素移动到它们在 DOM 中的祖父元素。

  5. 挑战:使用 jsperf.com/,粘贴 index.html 的内容并比较使用以下内容查找 <td id="release"> 的最近祖先表元素的性能:

  • .closest() 方法

  • .parents() 方法,将结果限制为找到的第一个表格

  1. 挑战:使用 jsperf.com/,粘贴 index.html 的内容并比较使用以下内容查找每一行中最后一个 <td> 元素的性能:
  • :last-child 伪类

  • :nth-child() 伪类

  • 每行内的.last()方法(使用.each()循环遍历行)

  • 每行内的:last伪类(使用.each()循环遍历行)

第十章:高级事件

要构建交互式的 Web 应用程序,我们需要观察用户的活动并对其做出响应。 我们已经看到,jQuery 的事件系统可以简化此任务,而且我们已经多次使用了这个事件系统。

在第三章,处理事件,我们提到了 jQuery 提供的一些用于对事件做出反应的功能。 在这一更高级的章节中,我们将涵盖:

  • 事件委托及其带来的挑战

  • 与某些事件相关的性能陷阱以及如何解决它们

  • 我们自己定义的自定义事件

  • jQuery 内部使用的特殊事件系统用于复杂的交互。

重新审视事件

对于我们的示例文档,我们将创建一个简单的照片画廊。 画廊将显示一组照片,并在点击链接时显示额外的照片。 我们还将使用 jQuery 的事件系统在鼠标悬停在照片上时显示每个照片的文本信息。 定义画廊的 HTML 如下所示:

<div id="container"> 
  <h1>Photo Gallery</h1> 

  <div id="gallery"> 
    <div class="photo"> 
      <img src="img/skyemonroe.jpg"> 
      <div class="details"> 
        <div class="description">The Cuillin Mountains, 
          Isle of Skye, Scotland.</div> 
        <div class="date">12/24/2000</div> 
        <div class="photographer">Alasdair Dougall</div> 
      </div> 
    </div> 
    <div class="photo"> 
      <img src="img/dscn1328.jpg"> 
      <div class="details"> 
        <div class="description">Mt. Ruapehu in summer</div> 
        <div class="date">01/13/2005</div> 
        <div class="photographer">Andrew McMillan</div> 
      </div> 
    </div> 
    <div class="photo"> 
      <img src="img/024.JPG"> 
      <div class="details"> 
        <div class="description">midday sun</div> 
        <div class="date">04/26/2011</div> 
        <div class="photographer">Jaycee Barratt</div> 
      </div> 
    </div> 
    <!-- Code continues --> 
  </div> 
  <a id="more-photos" href="pages/1.html">More Photos</a> 
</div> 

获取示例代码

您可以从以下 GitHub 存储库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3.

当我们对照片应用样式时,将它们排列成三行将使画廊看起来像以下屏幕截图:

加载更多数据页面

到目前为止,我们已经是对于页面元素点击的常见任务的专家了。当点击“更多照片”链接时,我们需要执行一个 Ajax 请求以获取下一组照片,并将它们附加到 <div id="gallery"> 如下所示:

$(() => {
  $('#more-photos')
    .click((e) => {
      e.preventDefault();
      const url = $(e.target).attr('href');

      $.get(url)
        .then((data) => {
          $('#gallery')
            .append(data);
        })
        .catch(({ statusText }) => {
          $('#gallery')
            .append(`<strong>${statusText}</strong>`)
        });
    });
});

列表 10.1

我们还需要更新“更多照片”链接的目标,以指向下一页照片:

$(() => {
  var pageNum = 1;

  $('#more-photos')
    .click((e) => {
      e.preventDefault();
      const $link = $(e.target);
      const url = $link.attr('href');

      if (pageNum > 19) {
        $link.remove();
        return;
      }

      $link.attr('href', `pages/${++pageNum}.html`);

      $.get(url)
        .then((data) => {
          $('#gallery')
            .append(data);
        })
        .catch(({ statusText }) => {
          $('#gallery')
            .append(`<strong>${statusText}</strong>`)
        });
    });
});

列表 10.2

我们的 .click() 处理程序现在使用 pageNum 变量来跟踪要请求的下一页照片,并使用它来构建链接的新 href 值。 由于 pageNum 在函数外部定义,因此它的值在链接的点击之间保持不变。 当我们到达最后一页照片时,我们会删除该链接。

我们还应考虑使用 HTML5 历史记录 API,以允许用户标记我们加载的 Ajax 内容。 您可以在 Dive into HTML5 (diveintohtml5.info/history.html) 了解有关此 API 的信息,并使用 History 插件 (github.com/browserstate/history.js) 很容易地实现它。

在悬停时显示数据

我们想要在此页面上提供的下一个功能是,当用户的鼠标位于页面的该区域时,显示与每张照片相关的详细信息。 对于显示此信息的首次尝试,我们可以使用 .hover() 方法:

$(() => {
  $('div.photo')
    .hover((e) => {
      $(e.currentTarget)
        .find('.details')
        .fadeTo('fast', 0.7);
  }, (e) => {
      $(e.currentTarget)
        .find('.details')
        .fadeOut('fast');
  });
}); 

列表 10.3

当光标进入照片的边界时,相关信息以 70% 的不透明度淡入,当光标离开时,信息再次淡出:

当然,执行此任务的方法有多种。由于每个处理程序的一部分是相同的,因此可以将两个处理程序合并。我们可以通过用空格分隔事件名称来同时绑定处理程序到mouseentermouseleave,如下所示:

 $('div.photo')
   .on('mouseenter mouseleave', (e) => {
     const $details = $(e.currentTarget).find('.details');

     if (e.type == 'mouseenter') {
       $details.fadeTo('fast', 0.7);
     } else {
       $details.fadeOut('fast');
     }
   });

列表 10.4

对于两个事件都绑定了相同处理程序,我们检查事件的类型以确定是淡入还是淡出详情。然而,定位<div>的代码对于两个事件是相同的,因此我们可以只写一次。

坦率地说,这个例子有点做作,因为此示例中的共享代码如此简短。但是,在其他情况下,这种技术可以显著减少代码复杂性。例如,如果我们选择在mouseenter上添加一个类,并在mouseleave上删除它,而不是动画化透明度,我们可以在处理程序内部用一个语句解决它,如下所示:

$(e.currentTarget)
  .find('.details') 
  .toggleClass('entered', e.type == 'mouseenter'); 

无论如何,我们的脚本现在正在按预期工作,除了我们还没有考虑用户点击更多照片链接时加载的附加照片。正如我们在第三章中所述,处理事件,事件处理程序仅附加到在我们进行.on()调用时存在的元素上。稍后添加的元素,例如来自 Ajax 调用的元素,不会具有行为。我们看到解决此问题的两种方法是在引入新内容后重新绑定事件处理程序,或者最初将处理程序绑定到包含元素并依赖事件冒泡。第二种方法,事件委托,是我们将在这里追求的方法。

事件委托

请记住,为了手动实现事件委托,我们会检查事件对象的target属性,以查看它是否与我们想要触发行为的元素匹配。事件目标表示接收事件的最内部或最深嵌套的元素。然而,这次我们的示例 HTML 提出了一个新的挑战。<div class="photo">元素不太可能是事件目标,因为它们包含其他元素,比如图像本身和图像详情。

我们需要的是.closest()方法,它会从父级元素向上遍历 DOM,直到找到与给定选择器表达式匹配的元素为止。如果找不到任何元素,则它会像任何其他 DOM 遍历方法一样,返回一个新的空 jQuery 对象。我们可以使用.closest()方法从任何包含它的元素中找到<div class="photo">,如下所示:

$(() => { 
  $('#gallery')
    .on('mouseover mouseout', (e) => {
      const $target = $(e.target)
        .closest('div.photo');
      const $related = $(e.relatedTarget)
        .closest('div.photo');
      const $details = $target
        .find('.details');

      if (e.type == 'mouseover' && $target.length) {
        $details.fadeTo('fast', 0.7);
      } else if (e == 'mouseout' && !$related.length) {
        $details.fadeOut('fast');
      }
    });
}); 

列表 10.5

请注意,我们还需要将事件类型从mouseentermouseleave更改为mouseovermouseout,因为前者仅在鼠标首次进入画廊<div>并最终离开时触发,我们需要处理程序在鼠标进入该包装<div>内的任何照片时被触发。但后者引入了另一种情况,即除非我们包含对event对象的relatedTarget属性的附加检查,否则详细信息<div>将重复淡入和淡出。即使有了额外的代码,快速重复的鼠标移动到照片上和移出照片时的处理也不令人满意,导致偶尔会出现详细信息<div>可见,而应该淡出。

使用 jQuery 的委托能力

当任务变得更加复杂时,手动管理事件委托可能会非常困难。幸运的是,jQuery 的.on()方法内置了委托,这可以使我们的生活变得更加简单。利用这种能力,我们的代码可以回到第 10.4 编列的简洁性:

$(() => { 
  $('#gallery')
    .on('mouseenter mouseleave', 'div.photo', (e) => {
      const $details = $(e.currentTarget).find('.details');

      if (e.type == 'mouseenter') {
        $details.fadeTo('fast', 0.7);
      } else {
        $details.fadeOut('fast');
      }
    });
}); 

第 10.6 编列

选择器#gallery第 10.5 编列保持不变,但事件类型返回到第 10.4 编列mouseentermouseleave。当我们将'div.photo'作为.on()的第二个参数传入时,jQuery 将e.currentTarget映射到'#gallery'中与该选择器匹配的元素。

选择委托范围

因为我们处理的所有照片元素都包含在<div id="gallery">中,所以我们在上一个示例中使用了#gallery作为我们的委托范围。然而,任何一个所有照片的祖先元素都可以用作这个范围。例如,我们可以将处理程序绑定到document,这是页面上所有内容的公共祖先:

$(() => {
  $(document)
    .on('mouseenter mouseleave', 'div.photo', (e) => {
      const $details = $(e.currentTarget).find('.details');

      if (e.type == 'mouseenter') {
        $details.fadeTo('fast', 0.7);
      } else {
        $details.fadeOut('fast');
      }
    });
}); 

第 10.7 编列

在设置事件委托时,将事件处理程序直接附加到document可能会很方便。由于所有页面元素都是从document继承而来的,我们不需要担心选择正确的容器。但是,这种便利可能会带来潜在的性能成本。

在深度嵌套的元素 DOM 中,依赖事件冒泡直到多个祖先元素可能是昂贵的。无论我们实际观察的是哪些元素(通过将它们的选择器作为.on()的第二个参数传递),如果我们将处理程序绑定到document,那么页面上发生的任何事件都需要被检查。例如,在第 10.6 编列中,每当鼠标进入页面上的任何元素时,jQuery 都需要检查它是否进入了一个<div class="photo">元素。在大型页面上,这可能会变得非常昂贵,特别是如果委托被大量使用。通过在委托上下文中更加具体,可以减少这种工作。

早期委托

尽管存在这些效率问题,但仍有理由选择将document作为我们的委托上下文。一般来说,我们只能在 DOM 元素加载后绑定事件处理程序,这就是为什么我们通常将代码放在$(() => {})内的原因。但是,document元素是立即可用的,因此我们无需等待整个 DOM 准备就绪才能绑定它。即使脚本被引用在文档的<head>中,就像我们的示例中一样,我们也可以立即调用.on(),如下所示:

(function($) { 
  $(document)
    .on('mouseenter mouseleave', 'div.photo', (e) => {
      const $details = $(e.currentTarget).find('.details');

      if (e.type == 'mouseenter') {
        $details.fadeTo('fast', 0.7);
      } else {
        $details.fadeOut('fast');
      }
    }); 
})(jQuery); 

图 10.8

因为我们不是在等待整个 DOM 准备就绪,所以我们可以确保mouseentermouseleave行为将立即适用于所有页面上呈现的<div class="photo">元素。

要看到这种技术的好处,考虑一个直接绑定到链接的click处理程序。假设此处理程序执行某些操作,并且还阻止链接的默认操作(导航到另一个页面)。如果我们等待整个文档准备就绪,我们将面临用户在处理程序注册之前单击该链接的风险,从而离开当前页面而不是得到脚本提供的增强处理。相比之下,将委托事件处理程序绑定到document使我们能够在不必扫描复杂的 DOM 结构的情况下提前绑定事件。

定义自定义事件

浏览器的 DOM 实现自然触发的事件对于任何交互式 Web 应用程序都至关重要。但是,在我们的 jQuery 代码中,我们不仅限于此事件集合。我们还可以添加自己的自定义事件。我们在第八章中简要介绍了这一点,开发插件,当我们看到 jQuery UI 小部件如何触发事件时,但在这里,我们将研究如何创建和使用自定义事件,而不是插件开发。

自定义事件必须由我们的代码手动触发。从某种意义上说,它们就像我们定义的常规函数一样,我们可以在脚本的另一个地方调用它时执行一块代码。对于自定义事件的.on()调用的行为类似于函数定义,而.trigger()调用的行为类似于函数调用。

但是,事件处理程序与触发它们的代码是解耦的。这意味着我们可以在任何时候触发事件,而无需预先知道触发时会发生什么。常规函数调用会导致执行单个代码块。但是,自定义事件可能没有处理程序,一个处理程序或许多处理程序绑定到它。无论如何,当事件被触发时,所有绑定的处理程序都将被执行。

为了说明这一点,我们可以修改我们的 Ajax 加载功能以使用自定义事件。每当用户请求更多照片时,我们将触发一个nextPage事件,并绑定处理程序来监视此事件并执行以前由.click()处理程序执行的工作:

$(() => { 
  $('#more-photos')
    .click((e) => {
      e.preventDefault();
      $(e.target).trigger('nextPage');
    });
}); 

列表 10.9

.click() 处理程序现在几乎不做任何工作。它触发自定义事件,并通过调用 .preventDefault() 阻止默认的链接行为。重要的工作转移到了对 nextPage 事件的新事件处理程序中,如下所示:

(($) => { 
  $(document)
    .on('nextPage', (e) => {
      $.get($(e.target).attr('href'))
        .then((data) => {
          $('#gallery')
            .append(data);
        })
        .catch(({ statusText }) => {
          $('#gallery')
            .append(`<strong>${statusText}</strong>`)
        });
    });

  var pageNum = 1;

  $(document)
    .on('nextPage', () => {
      if (pageNum > 19) {
        $('#more-photos').remove();
        return;
      }

      $('#more-photos')
        .attr('href', `pages/${++pageNum}.html`);
    });
})(jQuery); 

列表 10.10

自从 列表 10.2 以来,我们的代码并没有太多改变。最大的区别在于,我们将曾经的单个函数拆分为两个。这只是为了说明单个事件触发器可以导致多个绑定的处理程序触发。单击“更多照片”链接会导致下一组图片被追加,并且链接的 href 属性会被更新,如下图所示:

随着 列表 10.10 中的代码更改,我们还展示了事件冒泡的另一个应用。 nextPage 处理程序可以绑定到触发事件的链接上,但我们需要等到 DOM 准备就绪才能这样做。相反,我们将处理程序绑定到文档本身,这个文档立即可用,因此我们可以在 $(() => {}) 外部进行绑定。这实际上是我们在 列表 10.8 中利用的相同原理,当我们将 .on() 方法移到了 $(() => {}) 外部时。事件冒泡起作用,只要另一个处理程序不停止事件传播,我们的处理程序就会被触发。

无限滚动

正如多个事件处理程序可以对同一触发的事件作出反应一样,同一事件可以以多种方式触发。我们可以通过为页面添加无限滚动功能来演示这一点。这种技术允许用户的滚动条管理内容的加载,在用户达到到目前为止已加载内容的末尾时,获取更多内容。

我们将从一个简单的实现开始,然后在后续示例中改进它。基本思想是观察 scroll 事件,测量滚动时的当前滚动条位置,并在需要时加载新内容。以下代码将触发我们在 列表 10.10 中定义的 nextPage 事件:

(($) => { 
  const checkScrollPosition = () => {
    const distance = $(window).scrollTop() +
      $(window).height();

    if ($('#container').height() <= distance) {
      $(document).trigger('nextPage');
    }
  }

  $(() => {
    $(window)
      .scroll(checkScrollPosition)
      .trigger('scroll');
  }); 
})(jQuery); 

列表 10.11

我们在这里介绍的 checkScrollPosition() 函数被设置为窗口 scroll 事件的处理程序。此函数计算文档顶部到窗口底部的距离,然后将此距离与文档中主容器的总高度进行比较。一旦它们达到相等,我们就需要用额外的照片填充页面,因此我们触发 nextPage 事件。

一旦我们绑定了 scroll 处理程序,我们立即通过调用 .trigger('scroll') 触发它。这启动了这个过程,因此如果页面最初未填充照片,则立即进行 Ajax 请求以附加更多照片:

自定义事件参数

当我们定义函数时,我们可以设置任意数量的参数,以在实际调用函数时填充参数值。同样,当触发自定义事件时,我们可能想向任何注册的事件处理程序传递额外信息。我们可以通过使用自定义事件参数来实现这一点。

任何事件处理程序定义的第一个参数,正如我们所见,是 DOM 事件对象,由 jQuery 增强和扩展。我们定义的任何额外参数都可供自行决定使用。

要看到此功能的实际效果,我们将在 清单 10.10nextPage事件中添加一个新选项,允许我们向下滚动页面以显示新添加的内容:

(($) => { 
  $(document)
    .on('nextPage', (e, scrollToVisible) => {
      if (pageNum > 19) {
        $('#more-photos').remove();
        return;
      }

      $.get($('#more-photos').attr('href'))
        .then((data) => {
          const $data = $('#gallery')
            .append(data);

          if (scrollToVisible) {
            $(window)
              .scrollTop($data.offset().top);
          }

          checkScrollPosition();
    })
    .catch(({ statusText }) => {
      $('#gallery')
        .append(`<strong>${statusText}</strong>`)
    });
  }); 
})(jQuery); 

清单 10.12

现在,我们已经为事件回调添加了一个scrollToVisible参数。该参数的值决定了我们是否执行新功能,该功能包括测量新内容的位置并滚动到该位置。使用.offset()方法来进行测量非常容易,该方法返回新内容的顶部和左侧坐标。要向页面下移,我们调用.scrollTop()方法。

现在,我们需要向新参数传递一个参数。所需的一切就是在使用.trigger()调用事件时提供额外的值。当通过滚动触发newPage时,我们不希望出现新行为,因为用户已经直接操作了滚动位置。另一方面,当点击更多照片链接时,我们希望新添加的照片显示在屏幕上,因此我们将一个值为true传递给处理程序:

$(() => { 
  $('#more-photos')
    .click((e) => {
      e.preventDefault();
      $(e.target).trigger('nextPage', [true]);
    });
}); 

清单 10.13

在调用.trigger()时,我们现在提供了一个值数组以传递给事件处理程序。在这种情况下,值true将被传递到 清单 10.12 中事件处理程序的scrollToVisible参数。

请注意,自定义事件参数在交易的双方都是可选的。我们的代码中有两个对.trigger('nextPage')的调用,其中只有一个提供了参数值;当调用另一个时,这不会导致错误,而是处理程序中的每个参数都具有值undefined。同样,一个.on('nextPage')调用中缺少scrollToVisible参数也不是错误;如果在传递参数时不存在参数,那么该参数将被简单地忽略。

事件节流

我们在 清单 10.10 中实现的无限滚动功能的一个主要问题是性能影响。虽然我们的代码很简洁,但checkScrollPosition()函数确实需要做一些工作来测量页面和窗口的尺寸。这种努力可能会迅速积累,因为在一些浏览器中,scroll事件在滚动窗口时会重复触发。这种组合的结果可能是不流畅或性能低下。

几个本地事件有可能频繁触发。常见的罪魁祸首包括 scrollresizemousemove。为了解决这个问题,我们将实现事件节流。这种技术涉及限制我们的昂贵计算,使其仅在一些事件发生之后才发生,而不是每次都发生。我们可以更新我们的代码,以实现这种技术,如下所示:

$(() => { 
  var timer = 0;

  $(window)
    .scroll(() => {
      if (!timer) {
        timer = setTimeout(() => {
          checkScrollPosition();
          timer = 0;
        }, 250);
      }
    })
    .trigger('scroll');
}); 

清单 10.14

我们不直接将 checkScrollPosition() 设置为 scroll 事件处理程序,而是使用 JavaScript 的 setTimeout 函数将调用推迟了 250 毫秒。更重要的是,在做任何工作之前,我们首先检查是否有正在运行的计时器。由于检查一个简单变量的值非常快,我们的大多数事件处理程序调用几乎立即返回。checkScrollPosition() 调用只会在定时器完成时发生,最多每 250 毫秒一次。

我们可以轻松调整 setTimeout() 的值,以达到舒适的数值,从而在即时反馈和低性能影响之间取得合理的折中。我们的脚本现在是一个良好的网络公民。

其他执行节流的方式

我们实施的节流技术既高效又简单,但这并不是唯一的解决方案。根据节流的操作的性能特征和与页面的典型交互,我们可能需要建立页面的单个定时器,而不是在事件开始时创建一个定时器:

$(() => { 
  var scrolled = false;

  $(window)
    .scroll(() => {
      scrolled = true;
    });

  setInterval(() => {
    if (scrolled) {
      checkScrollPosition();
      scrolled = false;
    }
  }, 250);

  checkScrollPosition();
}); 

清单 10.15

与我们以前的节流代码不同,这种轮询解决方案使用一次 JavaScript setInterval() 函数调用来开始每250毫秒检查 scrolled 变量的状态。每次发生滚动事件时,scrolled 被设置为 true,确保下次间隔经过时将调用 checkScrollPosition()。其结果类似于清单 10.14

限制在频繁重复事件期间执行的处理量的第三种解决方案是去抖动。这种技术以电子开关发送的重复信号需要处理后的名字命名,确保即使发生了很多事件,也只有一个单一的最终事件被执行。我们将在第十三章高级 Ajax中看到这种技术的示例。

扩展事件

一些事件,如 mouseenterready,被 jQuery 内部指定为特殊事件。这些事件使用 jQuery 提供的复杂事件扩展框架。这些事件有机会在事件处理程序的生命周期中的各个时刻采取行动。它们可能会对绑定或解绑的处理程序做出反应,甚至可以有可阻止的默认行为,如点击链接或提交表单。事件扩展 API 允许我们创建类似于本机 DOM 事件的复杂新事件。

我们为Listing 10.13中的滚动实现的节流行为是有用的,我们可能想要将其推广到其他项目中使用。我们可以通过在特殊事件钩子内封装节流技术来实现这一点。

要为事件实现特殊行为,我们向$ .event.special对象添加一个属性。这个添加的属性本身是一个对象,它的键是我们的事件名称。它可以包含在事件生命周期中许多不同特定时间调用的回调函数,包括以下内容:

  • add: 每当为该事件的处理程序绑定时调用

  • remove: 每当为事件的处理程序解绑时调用

  • setup: 当为事件绑定处理程序时调用,但仅当没有为元素绑定该事件的其他处理程序时

  • teardown: 这是setup的反义词,当从元素解绑事件的最后一个处理程序时调用

  • _default: 这将成为事件的默认行为,在事件处理程序阻止默认操作之前调用

这些回调函数可以以一些非常有创意的方式使用。一个相当普遍的情景,我们将在我们的示例代码中探讨,就是根据浏览器条件自动触发事件。如果没有处理程序监听事件,监听状态并触发事件是很浪费的,所以我们可以使用setup回调仅在需要时启动这项工作:

(($) => { 
  $.event.special.throttledScroll = { 
    setup(data) { 
      var timer = 0; 
      $(this).on('scroll.throttledScroll', () => { 
        if (!timer) { 
          timer = setTimeout(() => { 
            $(this).triggerHandler('throttledScroll'); 
            timer = 0; 
          }, 250); 
        } 
      }); 
    }, 
    teardown() { 
      $(this).off('scroll.throttledScroll'); 
    } 
  }; 
})(jQuery); 

Listing 10.16

对于我们的滚动节流事件,我们需要绑定一个常规的scroll处理程序,该处理程序使用与我们在Listing 10.14中开发的相同的setTimeout技术。每当计时器完成时,将触发自定义事件。由于我们每个元素只需要一个计时器,因此setup回调将满足我们的需求。通过为scroll处理程序提供自定义命名空间,我们可以在调用teardown时轻松地移除处理程序。

要使用这种新行为,我们只需为throttledScroll事件绑定处理程序。这极大地简化了事件绑定代码,并为我们提供了一个非常可重用的节流机制,如下所示:

(($) => {
  $.event.special.throttledScroll = {
    setup(data) {
      var timer = 0;
      $(this)
        .on('scroll.throttledScroll', () => {
          if (!timer) {
            timer = setTimeout(() => {
              $(this).triggerHandler('throttledScroll');
              timer = 0;
            }, 250);
          }
        });
    },
    teardown() {
      $(this).off('scroll.throttledScroll');
    }
  };

  $(document)
    .on('mouseenter mouseleave', 'div.photo', (e) => {
      const $details = $(e.currentTarget).find('.details');

      if (e.type == 'mouseenter') {
        $details.fadeTo('fast', 0.7);
      } else {
        $details.fadeOut('fast');
      }
    });

  var pageNum = 1;

  $(document)
    .on('nextPage', (e, scrollToVisible) => {
      if (pageNum > 19) {
        $('#more-photos').remove();
        return;
      }

      $.get($('#more-photos').attr('href'))
        .then((data) => {
          const $data = $(data)
            .appendTo('#gallery');

          if (scrollToVisible) {
            $(window)
              .scrollTop($data.offset().top);
          }

          checkScrollPosition();
        })
       .catch(({ statusText }) => {
         $('#gallery')
           .append(`<strong>${statusText}</strong>`)
       });
    });

    $(document)
      .on('nextPage', () => {
        if (pageNum < 20) {
          $('#more-photos')
            .attr('href', `pages/${++pageNum}.html`);
        }
      });

    const checkScrollPosition = () => {
      const distance = $(window).scrollTop()
        + $(window).height();

      if ($('#container').height() <= distance) {
        $(document).trigger('nextPage');
      }
    };

  $(() => {
    $('#more-photos')
      .click((e) => {
        e.preventDefault();
        $(e.target).trigger('nextPage', [true]);
      });

    $(window)
      .on('throttledScroll', checkScrollPosition)
      .trigger('throttledScroll');
  });
})(jQuery);

Listing 10.17

关于特殊事件的更多信息

虽然本章涵盖了处理事件的高级技术,但事件扩展 API 确实非常先进,详细的调查超出了本书的范围。前面的throttledScroll示例涵盖了该功能的最简单和最常见的用法。其他可能的应用包括以下内容:

  • 修改事件对象,以便事件处理程序可以获得不同的信息

  • 导致在 DOM 中的一个位置发生的事件触发与不同元素相关联的行为

  • 对不是标准 DOM 事件的新的和特定于浏览器的事件做出反应,并允许 jQuery 代码对其做出反应,就像它们是标准的一样

  • 改变事件冒泡和委托的处理方式

这些任务中的许多都可能非常复杂。要深入了解事件扩展 API 提供的可能性,我们可以查阅 jQuery 学习中心的文档learn.jquery.com/events/event-extensions/

总结

如果我们选择充分利用 jQuery 事件系统,它可以非常强大。在本章中,我们已经看到了系统的几个方面,包括事件委托方法、自定义事件和事件扩展 API。我们还找到了绕过委托和频繁触发事件相关问题的方法。

进一步阅读

本书的附录 B,快速参考中提供了完整的事件方法列表,或者在官方的jQuery 文档中查看api.jquery.com/

练习

以下挑战练习可能需要使用官方 jQuery 文档api.jquery.com/

  1. 当用户点击照片时,在照片<div>上添加或删除selected类。确保即使是使用下一页链接后添加的照片,这种行为也能正常工作。

  2. 添加一个名为pageLoaded的新自定义事件,当新的图像集已添加到页面上时触发。

  3. 使用nextPagepageLoaded处理程序,仅在加载新页面时在页面底部显示一个加载消息。

  4. 将一个mousemove处理程序绑定到照片上,记录当前鼠标位置(使用console.log())。

  5. 修改此处理程序,以使日志记录不超过每秒五次。

  6. 挑战:创建一个名为tripleclick的新特殊事件,当鼠标按钮在 500 毫秒内点击三次时触发。为了测试该事件,将一个tripleclick处理程序绑定到<h1>元素上,该处理程序隐藏和显示<div id="gallery">的内容。

第十一章:高级效果

自从了解了 jQuery 的动画功能以来,我们发现了许多用途。我们可以轻松地隐藏和显示页面上的对象,我们可以优雅地调整元素的大小,我们可以平滑地重新定位元素。这个效果库是多功能的,包含的技术和专业能力甚至比我们迄今看到的还要多。

在第四章中,样式和动画,您学习了 jQuery 的基本动画功能。在这个更高级的章节中,我们将涵盖:

  • 收集关于动画状态的信息的方法

  • 中断活动动画的方法

  • 全局效果选项,可以一次性影响页面上的所有动画

  • Deferred 对象允许我们在动画完成后执行操作

  • 缓动,改变动画发生的速率

动画再访

为了刷新我们关于 jQuery 效果方法的记忆,我们将在本章中建立一个基线,从一个简单的悬停动画开始构建。使用带有照片缩略图的文档,当用户的鼠标悬停在上面时,我们将使每张照片略微增大,并在鼠标离开时恢复到原始大小。我们将使用的 HTML 标签目前还包含一些暂时隐藏的文本信息,稍后在本章中将使用:

<div class="team"> 
  <div class="member"> 
    <img class="avatar" src="img/rey.jpg" alt="" /> 
    <div class="name">Rey Bango</div> 
    <div class="location">Florida</div> 
    <p class="bio">Rey Bango is a consultant living in South Florida,        
    specializing in web application development...</p> 
  </div> 
  <div class="member"> 
    <img class="avatar" src="img/scott.jpg" alt="" /> 
    <div class="name">Scott González</div> 
    <div class="location">North Carolina</div> 
    <div class="position">jQuery UI Development Lead</div> 
    <p class="bio">Scott is a web developer living in Raleigh, NC...       </p> 
  </div> 
  <!-- Code continues ... --> 
</div> 

获取示例代码

您可以从以下 GitHub 存储库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

每张图像相关联的文本最初由 CSS 隐藏,通过将每个 <div> 移动到其 overflow: hidden 容器的左侧来实现:

.member { 
  position: relative; 
  overflow: hidden; 
} 

.member div { 
  position: absolute; 
  left: -300px; 
  width: 250px; 
} 

HTML 和 CSS 一起产生一个垂直排列的图像列表:

为了改变图像的大小,我们将把其高度和宽度从 75 像素增加到 85 像素。同时,为了保持图像居中,我们将其填充从 5 像素减少到 0 像素:

$(() => {
  $('div.member')
    .on('mouseenter mouseleave', ({ type, target }) => {
      const width = height = type == 'mouseenter' ?
        85 : 75;
      const paddingTop = paddingLeft = type == 'mouseenter' ?
        0 : 5;

      $(target)
        .find('img')
        .animate({
          width,
          height,
          paddingTop,
          paddingLeft
        });
    });
}); 

清单 11.1

在这里,我们重复了我们在第十章中看到的一种模式,高级事件,因为当鼠标进入区域时,我们执行的大部分工作与离开时相同;我们将 mouseentermouseleave 的处理程序合并为一个函数,而不是使用两个单独的回调调用 .hover()。在这个处理程序内部,我们根据触发的两个事件中的哪一个来确定 sizepadding 的值,并将这些属性值传递给 .animate() 方法。

当您看到将对象字面量表示法包围在函数参数 ({ type, target}) 周围时,这被称为对象解构。这只是一种方便的方法,可以从事件对象中获取我们需要的确切属性,从而在函数本身中编写更简洁的代码。

现在当鼠标光标位于图像上时,它比其他图像稍大:

观察和中断动画

我们的基本动画已经显示出一个问题。只要每次mouseentermouseleave事件后有足够的时间完成动画,动画就会按预期进行。然而,当鼠标光标快速移动并且事件被快速触发时,我们会看到图像在最后一个事件被触发后仍然反复变大和缩小。这是因为,如第四章所述,给定元素上的动画被添加到队列中并按顺序调用。第一个动画立即调用,按分配的时间完成,然后从队列中移除,此时下一个动画变为队列中的第一个,被调用,完成,被移除,依此类推,直到队列为空。

有许多情况下,jQuery 中称为fx的动画队列会引起期望的行为。但在我们这样的悬停动作中,需要绕过它。

确定动画状态

避免动画不良排队的一种方法是使用 jQuery 的自定义:animated选择器。在mouseenter/mouseleave事件处理程序中,我们可以使用该选择器来检查图像并查看它是否正在动画中:

$(() => {
  $('div.member')
    .on('mouseenter mouseleave', ({ type, target }) => {
      const width = height = type == 'mouseenter' ?
        85 : 75;
      const paddingTop = paddingLeft = type == 'mouseenter' ?
        0 : 5;

      $(target)
        .find('img')
        .not(':animated')
        .animate({
          width,
          height,
          paddingTop,
          paddingLeft
        });
      });
});

清单 11.2

当用户的鼠标进入成员<div>时,图像只有在没有被动画化时才会进行动画。当鼠标离开时,动画将无论其状态如何都会发生,因为我们始终希望最终将图像恢复到其原始尺寸和填充状态。

我们成功地避免了在清单 11.1中发生的无限动画,但是动画仍然需要改进。当鼠标快速进入和离开<div>标记时,图像仍然必须完成整个mouseenter动画(增大)才会开始mouseleave动画(缩小)。这肯定不是理想的情况,但是:animated伪类的测试引入了一个更大的问题:如果鼠标在图像缩小时进入<div>标记,那么图像将无法再次增大。只有在动画停止后,下一个mouseleavemouseenter动画才会执行另一个动画。在某些情况下使用:animated选择器可能很有用,但在这里并没有帮助太多。

停止运行的动画

幸运的是,jQuery 有一个方法可以帮助我们解决清单 11.2中显而易见的两个问题。.stop()方法可以立即停止动画。要使用它,我们可以将代码恢复到清单 11.1中的样子,然后在.find().animate()之间简单地插入.stop()

$(() => {
  $('div.member')
    .on('mouseenter mouseleave', ({ type, currentTarget }) => {
      const width = height = type == 'mouseenter' ?
        85 : 75;
      const paddingTop = paddingLeft = type == 'mouseenter' ?
        0 : 5;

      $(currentTarget)
        .find('img')
        .stop()
        .animate({
          width,
          height,
          paddingTop,
          paddingLeft
        });
    });
});

清单 11.3

值得注意的是,在进行新动画之前我们会在当前动画之前停止它。现在当鼠标重复进入和离开时,我们之前尝试的不良效果消失了。当前动画总是立即完成,因此fx队列中永远不会超过一个。当鼠标最终停下时,最终动画完成,因此图像要么完全增长(mouseenter),要么恢复到其原始尺寸(mouseleave),这取决于最后触发的事件。

停止动画时要小心

由于.stop()方法默认在当前位置停止动画,当与速记动画方法一起使用时可能会导致意外结果。在动画之前,这些速记方法确定最终值,然后对该值进行动画处理。例如,如果在其动画过程中使用.stop()停止.slideDown(),然后调用.slideUp(),那么下一次在元素上调用.slideDown()时,它只会滑动到上次停止的高度。为了减轻这种问题,.stop()方法可以接受两个布尔值(true/false)参数,第二个称为goToEnd。如果我们将此参数设置为true,则当前动画不仅停止,而且立即跳转到最终值。尽管如此,goToEnd功能可能会使动画看起来不流畅,因此更好的解决方案可能是将最终值存储在变量中,并显式地使用.animate()进行动画处理,而不是依赖 jQuery 来确定该值。

另一个 jQuery 方法.finish()可用于停止动画。它类似于.stop(true, true),因为它清除所有排队的动画,并将当前动画跳转到最终值。但是,与.stop(true, true)不同,它还会将所有排队的动画跳转到它们的最终值。

使用全局效果属性

jQuery 中的效果模块包含一个方便的$.fx对象,当我们想要全面改变动画特性时可以访问该对象。虽然该对象的一些属性未记录,并且只能在库内部使用,但其他属性则作为工具提供,用于全局改变动画运行方式。在以下示例中,我们将看一些已记录属性。

禁用所有效果

我们已经讨论了如何停止当前正在运行的动画,但是如果我们需要完全禁用所有动画怎么办?例如,我们可能希望默认情况下提供动画,但是在低资源设备(动画可能看起来断断续续)或对于发现动画分散注意力的用户中禁用这些动画。为此,我们只需将$.fx.off属性设置为true。为了演示,我们将显示一个之前隐藏的按钮,以允许用户切换动画的开启和关闭:

$(() => {
  $('#fx-toggle')
    .show()
    .on('click', () => {
      $.fx.off = !$.fx.off;
    });
}); 

列表 11.4

隐藏按钮显示在介绍段落和随后的图像之间:

当用户点击按钮将动画切换关闭时,随后的动画,如我们的放大和缩小图像,将立即发生(持续时间为0毫秒),然后立即调用任何回调函数。

定义效果持续时间

$.fx对象的另一个属性是speeds。该属性本身是一个对象,由 jQuery 核心文件证实,由三个属性组成:

speeds: { 
  slow: 600, 
  fast: 200, 
  // Default speed 
  _default: 400 
} 

您已经学会了 jQuery 的所有动画方法都提供了一个可选的速度或持续时间参数。查看$.fx.speeds对象,我们可以看到字符串slowfast分别映射到 600 毫秒和 200 毫秒。每次调用动画方法时,jQuery 按照以下顺序执行以下步骤来确定效果的持续时间:

  1. 它检查$.fx.off是否为true。如果是,它将持续时间设置为0

  2. 它检查传递的持续时间是否为数字。如果是,则将持续时间设置为该数字的毫秒数。

  3. 它检查传递的持续时间是否匹配$.fx.speeds对象的属性键之一。如果是,则将持续时间设置为属性的值。

  4. 如果持续时间未由上述任何检查设置,则将持续时间设置为$.fx.speeds._default的值。

综合这些信息,我们现在知道,传递除slowfast之外的任何字符串持续时间都会导致持续时间为 400 毫秒。我们还可以看到,添加我们自己的自定义速度就像添加另一个属性到$.fx.speeds一样简单。例如,如果我们写$.fx.speeds.crawl = 1200,我们可以在任何动画方法的速度参数中使用'crawl'以运行动画 1200 毫秒,如下所示:

$(someElement).animate({width: '300px'}, 'crawl'); 

尽管键入'crawl'不比键入1200更容易,但在较大的项目中,当许多共享某个速度的动画需要更改时,自定义速度可能会派上用场。在这种情况下,我们可以更改$.fx.speeds.crawl的值,而不是在整个项目中搜索1200并仅在表示动画速度时替换每个值。

虽然自定义速度可能很有用,但也许更有用的是能够更改默认速度的能力。我们可以通过设置_default属性来做到这一点:

$.fx.speeds._default = 250; 

列表 11.5

现在,我们已经定义了一个新的更快的默认速度,除非我们覆盖它们的持续时间,否则任何新添加的动画都将使用它。为了看到这个过程,我们将向页面引入另一个交互元素。当用户点击其中一个肖像时,我们希望显示与该人物相关联的详细信息。我们将通过将它们从肖像下面移出到最终位置来创建详细信息从肖像中展开的错觉:

$(() => { 
  const showDetails = ({ currentTarget }) => {
    $(currentTarget)
      .find('div')
      .css({
        display: 'block',
        left: '-300px',
        top: 0
      })
      .each((i, element) => {
        $(element)
          .animate({
            left: 0,
            top: 25 * i
          });
      });
  }; 
  $('div.member').click(showDetails); 
}); 

列表 11.6

当点击成员时,我们使用showDetails()函数作为处理程序。该函数首先将详细信息<div>元素设置在成员肖像的下方的起始位置。然后将每个元素动画到其最终位置。通过调用.each(),我们可以计算每个元素的单独最终top位置。

动画完成后,详细信息文本可见:

由于.animate()方法调用是在不同的元素上进行的,所以它们是同时进行的,而不是排队进行的。而且,由于这些调用没有指定持续时间,它们都使用了新的默认持续时间 250 毫秒。

当点击另一个成员时,我们希望隐藏先前显示的成员。我们可以轻松地通过类来跟踪当前屏幕上显示的详细信息:

 const showDetails = ({ currentTarget }) => {
   $(currentTarget)
     .siblings('.active')
     .removeClass('active')
     .children('div')
     .fadeOut()
     .end()
     .end()
     .addClass('active')
     .find('div')
     .css({
       display: 'block',
       left: '-300px',
       top: 0
     })
     .each((i, element) => {
       $(element)
         .animate({
           left: 0,
           top: 25 * i
         });
     });
}; 

列表 11.7

哎呀!十个函数链接在一起?等等,这其实可能比拆分它们更好。首先,像这样链接调用意味着不需要使用临时变量来保存中间的 DOM 值。相反,我们可以一行接一行地读取以了解发生了什么。现在让我们逐个解释一下这些:

  • .siblings('.active'): 这会找到活动的<div>兄弟元素。

  • .removeClass('active'): 这会移除.active类。

  • .children('div'): 这会找到子<div>元素。

  • .fadeOut(): 这会将它们移除。

  • .end(): 这会清除.children('div')查询结果。

  • .end(): 这会清除.siblings('.active')查询结果。

  • .addClass('active'): 这会将.active类添加到事件目标,即容器<div>上。

  • .find('div'): 这会找到所有子<div>元素以显示。

  • .css(): 这会设置相关的显示 CSS。

  • .each(): 这会向topleftCSS 属性添加动画。

请注意,我们的.fadeOut()调用也使用了我们定义的更快的 250 毫秒持续时间。默认值适用于 jQuery 的预打包效果,就像它们适用于自定义.animate()调用一样。

多属性缓动

showDetails()函数几乎实现了我们想要的展开效果,但由于topleft属性以相同的速率进行动画,它看起来更像是一个滑动效果。我们可以通过仅为top属性更改缓动方程式为easeInQuart来微妙地改变效果,从而使元素沿着曲线路径而不是直线路径移动。但请记住,除了swinglinear之外的任何缓动都需要插件,例如 jQuery UI 的效果核心(jqueryui.com/)。

.each((i, element) => {
  $(element)
    .animate({
      left: 0,
      top: 25 * i
    },{
      duration: 'slow',
      specialEasing: {
        top: 'easeInQuart'
      }
    });
 });

列表 11.8

specialEasing选项允许我们为每个正在动画化的属性设置不同的加速曲线。如果选项中不包括的属性,则将使用easing选项的方程式(如果提供)或默认的swing方程式。

现在我们有了一个引人注目的动画,展示了与团队成员相关的大部分细节。但我们还没有展示成员的传记。在这之前,我们需要稍微偏离一下话题,谈谈 jQuery 的延迟对象机制。

使用延迟对象

有时,我们会遇到一些情况,我们希望在过程完成时采取行动,但我们并不一定知道这个过程需要多长时间,或者是否会成功。为了处理这些情况,jQuery 为我们提供了延迟对象(promises)。延迟对象封装了需要一些时间来完成的操作。

可以随时通过调用$.Deferred()构造函数创建一个新的延迟对象。一旦我们有了这样的对象,我们可以执行长时间运行的操作,然后在对象上调用.resolve().reject()方法来指示操作是否成功或失败。然而,手动这样做有点不寻常。通常,我们不是手动创建自己的延迟对象,而是 jQuery 或其插件会创建对象,并负责解决或拒绝它。我们只需要学习如何使用创建的对象。

我们不打算详细介绍$.Deferred()构造函数的操作方式,而是在这里重点讨论 jQuery 效果如何利用延迟对象。在第十三章中,高级 Ajax,我们将进一步探讨在 Ajax 请求的背景下的延迟对象。

每个延迟对象都承诺向其他代码提供数据。这个承诺作为另一个具有自己一套方法的对象来表示。从任何延迟对象,我们可以通过调用它的.promise()方法来获得它的 promise 对象。然后,我们可以调用 promise 的方法来附加处理程序,当 promise 被履行时执行:

  • .then()方法附加了一个处理程序,当延迟对象成功解决时调用。

  • .catch()方法附加了一个处理程序,当延迟对象被拒绝时调用。

  • .always()方法附加了一个处理程序,当延迟对象完成其任务时被调用,无论是被解决还是被拒绝。

这些处理程序非常类似于我们提供给.on()的回调函数,因为它们是在某个事件发生时调用的函数。我们还可以附加多个处理程序到同一个承诺上,所有的会在适当的时候被调用。然而,这里也有一些重要的区别。承诺处理程序只会被调用一次;延迟对象无法再次解决。如果在我们附加处理程序时延迟对象已经被解决,那么承诺处理程序也会立即被调用。

在第六章中,使用 Ajax 发送数据,我们看到了一个非常简单的例子,说明了 jQuery 的 Ajax 系统如何使用延迟对象。现在,我们将再次利用这个强大的工具,通过研究 jQuery 动画系统创建的延迟对象来使用它。

动画的承诺

每个 jQuery 集合都有一组延迟对象与其关联,用于跟踪集合中元素的排队操作的状态。通过在 jQuery 对象上调用 .promise() 方法,我们得到一个在队列完成时解析的 promise 对象。特别是,我们可以使用此 promise 在任何匹配元素上运行的所有动画完成时采取行动。

就像我们有一个 showDetails() 函数来显示成员的名称和位置信息一样,我们可以编写一个 showBio() 函数来显示传记信息。但首先,我们将向 <body> 标签附加一个新的 <div> 标签并设置两个选项对象:

$(() => {
  const $movable = $('<div/>')
    .attr('id', 'movable')
    .appendTo('body');

  const bioBaseStyles = {
    display: 'none',
    height: '5px',
    width: '25px'
  }

  const bioEffects = {
    duration: 800,
    easing: 'easeOutQuart',
    specialEasing: {
      opacity: 'linear'
    }
  };
});

11.9 清单

这个新的可移动 <div> 元素是我们实际上将要动画化的元素,在注入了传记副本后。像这样拥有一个包装元素在动画化元素的宽度和高度时特别有用。我们可以将其 overflow 属性设置为 hidden,并为其中的传记设置显式的宽度和高度,以避免在我们动画化传记 <div> 元素本身时持续不断地重新排列文本。

我们将使用 showBio() 函数根据点击的成员图像确定可移动 <div> 的起始和结束样式。请注意,我们使用 $.extend() 方法将保持不变的一组基本样式与根据成员位置变化的 topleft 属性进行合并。然后,只需使用 .css() 设置起始样式和 .animate() 设置结束样式:

const showBio = (target) => {
  const $member = $(target).parent();
  const $bio = $member.find('p.bio');
  const startStyles = $.extend(
    {},
    bioBaseStyles,
    $member.offset()
  );
  const endStyles = {
    width: $bio.width(),
    top: $member.offset().top + 5,
    left: $member.width() + $member.offset().left - 5,
    opacity: 'show'
  };

  $movable
    .html($bio.clone())
    .css(startStyles)
    .animate(endStyles, bioEffects)
    .animate(
      { height: $bio.height() },
      { easing: 'easeOutQuart' }
    );
}; 

11.10 清单

我们排队了两个 .animate() 方法,以便传记首先从左侧飞出并变宽和完全不透明,然后在到位后向下滑动到其完整高度。

在 第四章,样式和动画 中,我们看到 jQuery 动画方法中的回调函数在集合中每个元素的动画完成时被调用。我们希望在其他 <div> 元素出现后显示成员的传记。在 jQuery 引入 .promise() 方法之前,这将是一项繁重的任务,需要我们在每次执行回调时从总元素数倒计时,直到最后一次,此时我们可以执行动画化传记的代码。

现在我们可以简单地将 .promise().then() 方法链接到我们的 showDetails() 函数内部的 .each() 方法中:

const showDetails = ({ currentTarget }) => {
  $(currentTarget)
    .siblings('.active')
    .removeClass('active')
    .children('div')
    .fadeOut()
    .end()
    .end()
    .addClass('active')
    .find('div')
    .css({
      display: 'block',
      left: '-300px',
      top: 0
    })
    .each((i, element) => {
      $(element)
        .animate({
          left: 0,
          top: 25 * i
        },{
          duration: 'slow',
          specialEasing: {
            top: 'easeInQuart'
          }
        });
    })
    .promise()
    .then(showBio);
}; 

11.11 清单

.then() 方法将我们的 showBio() 函数的引用作为其参数。现在,点击图像将以吸引人的动画序列将所有成员信息显示出来:

自 jQuery 3.0 起,promise() 方法返回的 promises 与原生 ES 2015 promises 完全兼容。这意味着在可能的情况下,我们应该使用相同的 API。例如,使用 then() 代替 done()。它们做的是一样的事情,你的异步代码将与其他异步代码保持一致。

对动画进行细粒度控制

即使我们已经研究了许多高级功能,jQuery 的效果模块还有很多可以探索的地方。jQuery 1.8 的重写为这个模块引入了许多高级开发者调整各种效果甚至更改驱动动画的底层引擎的方法。例如,除了提供 durationeasing 等选项外,.animate() 方法还提供了一些回调选项,让我们在动画的每一步检查和修改动画:

$('#mydiv').animate({ 
  height: '200px', 
  width: '400px' 
}, { 
  step(now, tween) { 
   // monitor height and width 
   // adjust tween properties 
  }, 
  progress(animation, progress, remainingMs) {} 
}); 

step() 函数,每次动画属性动画期间大约每 13 毫秒调用一次,允许我们根据传递的 now 参数的当前值调整 tween 对象的属性,如结束值、缓动类型或实际正在动画的属性。例如,一个复杂的演示可能会使用 step() 函数来检测两个移动元素之间的碰撞,并根据碰撞调整它们的轨迹。

progress() 函数在动画的生命周期中被多次调用:

  • 它与 step() 不同之处在于,它每一步仅在每个元素上调用一次,而不管正在动画多少个属性

  • 它提供了动画的不同方面,包括动画的 promise 对象、进度(一个介于 01 之间的数字)以及动画中剩余的毫秒数。

所有 jQuery 的动画都使用一个名为 setTimeout() 的 JavaScript 计时器函数来重复调用函数 —— 默认情况下每 13 毫秒一次 —— 并在每个时刻改变样式属性。然而,一些现代浏览器提供了一个新的 requestAnimationFrame() 函数,它相对于 setTimeout() 有一些优势,包括增加了精度(因此动画的平滑度更高)和改善了移动设备的电池消耗。

在 jQuery 的动画系统的最低级别上,有它的 $.Animation()$.Tween() 函数。这些函数及其对应的对象可以用来调整动画的每一个可能的方面。例如,我们可以使用 $.Animation 来创建一个动画预处理。这样的预处理可以采用一个

特别

基于传递给 .animate() 方法的 options 对象中的属性的存在,在动画结束时执行动作:

$.Animation.prefilter(function(element, properties, options) { 
  if (options.removeAfter) { 
    this.done(function () { 
      $(element).remove(); 
    }); 
  } 
}); 

使用这段代码,调用 $('#my-div').fadeOut({ removeAfter: true }) 将在淡出完成后自动从 DOM 中删除 <div>

摘要

在本章中,我们进一步研究了几种可以帮助我们制作对用户有用的漂亮动画的技术。我们现在可以单独控制我们正在动画化的每个属性的加速度和减速度,并在需要时单独或全局停止这些动画。我们了解了 jQuery 的效果库内部定义的属性,以及如何更改其中一些属性以适应我们的需求。我们初次涉足了 jQuery 延迟对象系统,我们将在第十三章 高级 Ajax中进一步探索,并且我们品尝到了调整 jQuery 动画系统的许多机会。

进一步阅读

本书附录 B 中提供了完整的效果和动画方法列表,或者您可以在官方 jQuery 文档中找到。

练习

挑战练习可能需要使用官方 jQuery 文档

  1. 定义一个名为zippy的新动画速度常数,并将其应用于传记显示效果。

  2. 更改成员详细信息的水平移动的缓动,使其反弹到位。

  3. 向 promise 添加一个第二个延迟回调函数,将highlight类添加到当前成员位置的<div>中。

  4. 挑战:在动画传记之前添加两秒的延迟。使用 jQuery 的.delay()方法。

  5. 挑战:当点击活动照片时,折叠生物详细信息。在执行此操作之前停止任何正在运行的动画。

第十二章:高级 DOM 操作

在本书中,我们已经使用了 jQuery 强大的 DOM 操作方法来改变文档的内容。我们已经看到了几种插入新内容、移动现有内容或完全删除内容的方法。我们也知道如何更改元素的属性和属性以满足我们的需求。

在第五章 操作 DOM 中,我们介绍了这些重要技术。在这个更高级的章节中,我们将涵盖:

  • 使用 .append() 排序页面元素

  • 附加自定义数据到元素

  • 读取 HTML5 数据属性

  • 从 JSON 数据创建元素

  • 使用 CSS 钩子扩展 DOM 操作系统

排序表格行

在本章中,我们正在研究的大多数主题都可以通过对表格行进行排序来演示。这个常见的任务是帮助用户快速找到他们所需信息的有效方法。当然,有许多方法可以做到这一点。

在服务器上排序表格

数据排序的常见解决方案是在服务器上执行。表格中的数据通常来自数据库,这意味着从数据库中提取数据的代码可以请求以给定的排序顺序(例如,使用 SQL 语言的 ORDER BY 子句)提取数据。如果我们有服务器端代码可供使用,那么从一个合理的默认排序顺序开始是很简单的。

但是,当用户可以确定排序顺序时,排序就变得最有用了。这方面的常见用户界面是将可排序列的表头(<th>)转换为链接。这些链接可以指向当前页面,但附加了一个查询字符串来指示按哪一列排序,如下面的代码片段所示:

<table id="my-data"> 
  <thead> 
    <tr> 
      <th class="name"> 
        <a href="index.php?sort=name">Name</a> 
      </th> 
      <th class="date"> 
        <a href="index.php?sort=date">Date</a> 
      </th> 
    </tr> 
  </thead> 
  <tbody> 
    ... 
  </tbody> 
</table> 

服务器可以通过返回数据库内容的不同顺序来响应查询字符串参数。

使用 Ajax 排序表格

这个设置很简单,但是每次排序操作都需要页面刷新。正如我们所见,jQuery 允许我们通过使用 Ajax 方法来消除这种页面刷新。如果我们像以前一样将列标题设置为链接,我们可以添加 jQuery 代码来将那些链接转换为 Ajax 请求:

$(() => { 
  $('#my-data th a')
    .click((e) => { 
      e.preventDefault(); 
      $('#my-data tbody')
        .load($(e.target).attr('href')); 
    }); 
}); 

当锚点被点击时,现在 jQuery 会向服务器发送一个 Ajax 请求以获取相同的页面。当 jQuery 用于使用 Ajax 发送页面请求时,它会将 X-Requested-With HTTP 头设置为 XMLHttpRequest,以便服务器可以确定正在进行 Ajax 请求。当此参数存在时,服务器代码可以编写为仅在回送 <tbody> 元素本身的内容,而不是周围的页面。通过这种方式,我们可以使用响应来替换现有 <tbody> 元素的内容。

这是渐进增强的一个例子。页面即使没有任何 JavaScript 也能正常工作,因为仍然存在用于服务器端排序的链接。但是,当 JavaScript 可用时,我们会劫持页面请求,允许排序而无需完全重新加载页面。

在浏览器中排序表

但是有时候,当我们在排序时不想等待服务器响应或者没有服务器端脚本语言可用时。在这种情况下,一个可行的替代方法是完全在浏览器中使用 JavaScript 和 jQuery 的 DOM 操作方法进行排序。

为了演示本章中的各种技术,我们将设置三个单独的 jQuery 排序机制。每个都将以独特的方式完成相同的目标。我们的示例将使用以下方法对表进行排序:

  • 从 HTML 内容中提取的数据

  • HTML5 自定义数据属性

  • 表数据的 JSON 表示

我们将要排序的表具有不同的 HTML 结构,以适应不同的 JavaScript 技术,但每个表都包含列出书籍、作者姓名、发布日期和价格的列。第一个表具有简单的结构:

<table id="t-1" class="sortable"> 
  <thead> 
    <tr> 
      <th></th> 
      <th class="sort-alpha">Title</th> 
      <th class="sort-alpha">Author(s)</th> 
      <th class="sort-date">Publish Date</th> 
      <th class="sort-numeric">Price</th> 
    </tr> 
  </thead> 
  <tbody> 
    <tr> 
      <td><img src="img/2862_OS.jpg" alt="Drupal 7"></td> 
      <td>Drupal 7</td> 
      <td>David <span class="sort-key">Mercer</span></td> 
      <td>September 2010</td> 
      <td>$44.99</td> 
    </tr> 
    <!-- code continues --> 
  </tbody> 
</table> 

获取示例代码

您可以从以下 GitHub 代码库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

在我们用 JavaScript 增强表格之前,前几行如下所示:

移动和插入元素的再次访问

在接下来的示例中,我们将构建一个灵活的排序机制,可以在每一列上工作。为此,我们将使用 jQuery 的 DOM 操作方法来插入一些新元素并将其他现有元素移动到 DOM 中的新位置。我们将从最简单的部分开始--链接表头。

在现有文本周围添加链接

我们想将表头转换为按其各自列排序数据的链接。我们可以使用 jQuery 的 .wrapInner() 方法来添加它们;我们回想起 第五章 DOM 操作 中,.wrapInner() 将一个新元素(在本例中为 <a> 元素) 插入 匹配的元素内,但在周围子元素:

$(() => {
  const $headers = $('#t-1')
    .find('thead th')
    .slice(1);

  $headers
    .wrapInner($('<a/>').attr('href', '#'))
    .addClass('sort');
});

列表 12.1

我们跳过了每个表的第一个 <th> 元素(使用 .slice())因为它除了空格之外没有文本,因此没有必要对封面照片进行标记或排序。然后,我们对剩余的 <th> 元素添加了一个 sort 类,以便在 CSS 中将其与不可排序的元素区分开。现在,标题行如下所示:

这是渐进增强的对应,优雅降级的一个例子。与前面讨论的 Ajax 解决方案不同,这种技术在没有 JavaScript 的情况下无法工作;我们假设服务器在这个例子中没有可用于目的的脚本语言。由于 JavaScript 是必需的,以使排序工作,我们只通过代码添加 sort 类和锚点,从而确保界面只在脚本运行时表明可以排序。而且,由于我们实际上是创建链接而不仅仅是添加视觉样式以指示标题可以点击,因此我们为需要使用键盘导航到标题的用户提供了额外的辅助功能(通过按Tab键)。页面退化为一个仍然可以使用但无法进行排序的页面。

对简单的 JavaScript 数组进行排序

为了进行排序,我们将利用 JavaScript 的内置.sort()方法。它对数组进行原地排序,并可以接受一个比较器函数作为参数。此函数比较数组中的两个项目,并根据应该在排序后的数组中排在前面的项目返回正数或负数。

例如,取一个简单的数字数组:

const arr = [52, 97, 3, 62, 10, 63, 64, 1, 9, 3, 4]; 

我们可以通过调用 arr.sort() 来对该数组进行排序。之后,项目的顺序如下:

[1, 10, 3, 3, 4, 52, 62, 63, 64, 9, 97] 

默认情况下,如我们在这里看到的,项目按字母顺序(按字母顺序)排序。在这种情况下,可能更合理地按数字排序。为此,我们可以向 .sort() 方法提供一个比较函数:

arr.sort((a, b) => a < b ? -1 : (a > b ? 1 : 0)); 

此函数如果 a 应该在排序后的数组中排在 b 之前,则返回负数;如果 b 应该在 a 之前,则返回正数;如果项目的顺序无关紧要,则返回零。有了这些信息,.sort() 方法可以适当地对项目进行排序:

[1, 3, 3, 4, 9, 10, 52, 62, 63, 64, 97] 

接下来,我们将这个.sort()方法应用到我们的表格行上。

对 DOM 元素进行排序

让我们对表格的 Title 列执行排序。请注意,虽然我们将 sort 类添加到它和其他列,但此列的标题单元格已经有一个由 HTML 提供的 sort-alpha 类。其他标题单元格根据每个排序类型接受了类似的处理,但现在我们将专注于 Title 标题,它需要一个简单的按字母顺序排序:

$(() => {
  const comparator = (a, b) => a < b ? -1 : (a > b ? 1 : 0);
  const sortKey = (element, column) => $.trim($(element)
    .children('td')
    .eq(column)
    .text()
    .toUpperCase()
  );

  $('#t-1')
    .find('thead th')
    .slice(1)
    .wrapInner($('<a/>').attr('href', '#'))
    .addClass('sort')
    .on('click', (e) => {
      e.preventDefault();

      const column = $(e.currentTarget).index();

      $('#t-1')
        .find('tbody > tr')
        .get()
        .sort((a, b) => comparator(
          sortKey(a, column),
          sortKey(b, column)
        ))
        .forEach((element) => {
          $(element)
            .parent()
            .append(element);
        });
    });
}); 

列表 12.2

一旦我们找到了点击的标题单元格的索引,我们就会检索所有数据行的数组。这是一个很好的例子,说明了.get()如何将 jQuery 对象转换为 DOM 节点数组;尽管 jQuery 对象在许多方面都像数组一样,但它们并没有所有可用的本机数组方法,比如.pop().shift()

在内部,jQuery 实际上定义了一些类似原生数组方法的方法。例如,.sort().push().splice() 都是 jQuery 对象的方法。然而,由于这些方法是内部使用的,并且没有公开文档记录,我们不能指望它们在我们自己的代码中以预期的方式运行,因此应避免在 jQuery 对象上调用它们。

现在我们有了一个 DOM 节点数组,我们可以对它们进行排序,但要做到这一点,我们需要编写一个适当的比较器函数。我们想根据相关表格单元格的文本内容对行进行排序,因此这将是比较器函数要检查的信息。我们知道要查看哪个单元格,因为我们使用 .index() 调用捕获了列索引。我们使用 jQuery 的 $.trim() 函数去除前导和尾随空格,然后将文本转换为大写,因为 JavaScript 中的字符串比较是区分大小写的,而我们的排序应该是不区分大小写的。

现在我们的数组已经排序了,但请注意,对 .sort() 的调用并没有改变 DOM 本身。要做到这一点,我们需要调用 DOM 操作方法来移动行。我们一次移动一行,将每行重新插入表格中。由于 .append() 不会克隆节点,而是移动它们,因此我们的表格现在已经排序了:

将数据存储在 DOM 元素旁边

我们的代码可以运行,但速度相当慢。问题在于比较器函数,它执行了大量的工作。在排序过程中,这个比较器将被调用多次,这意味着它需要很快。

数组排序性能

JavaScript 使用的实际排序算法没有在标准中定义。它可能是一个简单的排序,比如冒泡排序(在计算复杂度方面的最坏情况是 Θ(n²)),或者更复杂的方法,比如快速排序(平均情况下是 Θ(n log n))。不过可以肯定的是,将数组中的项数翻倍将会使比较器函数被调用的次数增加超过两倍。

解决我们慢比较器的方法是预先计算比较所需的键。我们可以在初始循环中完成大部分昂贵的工作,并使用 jQuery 的 .data() 方法将结果存储起来,该方法用于设置或检索与页面元素相关联的任意信息。然后我们只需在比较器函数中检查这些键,我们的排序就会明显加快:

$('#t-1')
  .find('thead th')
  .slice(1)
  .wrapInner($('<a/>').attr('href', '#'))
  .addClass('sort')
  .on('click', (e) => {
    e.preventDefault();

    const column = $(e.currentTarget).index();

    $('#t-1')
      .find('tbody > tr')
      .each((i, element) => {
        $(element)
          .data('sortKey', sortKey(element, column));
      })
      .get()
      .sort((a, b) => comparator(
        $(a).data('sortKey'),
        $(b).data('sortKey')
      ))
      .forEach((element) => {
        $(element)
          .parent()
          .append(element);
      });
  }); 

列表 12.3

.data() 方法和它的补充 .removeData() 提供了一个数据存储机制,它是一种方便的替代方案,用于扩展属性,或者直接添加到 DOM 元素的非标准属性。

执行额外的预计算

现在我们希望将相同类型的排序行为应用于我们表格的作者一栏。因为表头单元格具有sort-alpha类,作者一栏可以使用我们现有的代码进行排序。但理想情况下,作者应该按照姓氏而不是名字排序。由于一些书籍有多位作者,有些作者列出了中间名或缩写,我们需要外部指导来确定要用作排序键的文本部分。我们可以通过在单元格中包装相关部分来提供这些指导:

<td>David <span class="sort-key">Mercer</span></td> 

现在我们必须修改我们的排序代码,以考虑这个标记,而不影响Title列的现有行为,因为它已经运行良好。通过将标记排序键放在之前计算过的键的前面,我们可以先按照姓氏排序,如果指定的话,但是在整个字符串上作为后备进行排序:

const sortKey = (element, column) => {
  const $cell = $(element)
    .children('td')
    .eq(column);
  const sortText = $cell
    .find('span.sort-key')
    .text();
  const cellText = $cell
    .text()
    .toUpperCase();

  return $.trim(`${sortText} ${cellText}`);
}; 

列表 12.4

现在按照作者一栏对提供的键进行排序,从而按照姓氏排序:

如果两个姓氏相同,则排序会使用整个字符串作为定位的决定因素。

存储非字符串数据

我们的用户应该能够不仅按照标题和作者一栏进行排序,还可以按照发布日期和价格一栏进行排序。由于我们简化了比较函数,它可以处理各种类型的数据,但首先计算出的键需要针对其他数据类型进行调整。例如,在价格的情况下,我们需要去掉前导的$字符并解析剩余部分,以便我们可以进行数字比较:

var key = parseFloat($cell.text().replace(/^[^\d.]*/, '')); 
if (isNaN(key)) { 
  key = 0; 
} 

此处使用的正则表达式除了数字和小数点以外的任何前导字符,将结果传递给parseFloat()。然后需要检查parseFloat()的结果,因为如果无法从文本中提取数字,将返回NaN不是一个数字)。这可能对.sort()造成严重影响,所以将任何非数字设为0

对于日期单元格,我们可以使用 JavaScript 的 Date 对象:

var key = Date.parse(`1 ${$cell.text()}`); 

此表中的日期仅包含月份和年份; Date.parse()需要一个完全规定的日期。为了适应这一点,我们在字符串前面加上1,这样September 2010就变成了1 September 2010。现在我们有了一个完整的日期,Date.parse()可以将其转换为时间戳,可以使用我们正常的比较器进行排序。

我们可以将这些表达式放入三个单独的函数中,以便稍后可以根据应用于表头的类调用适当的函数:

const sortKeys = {
  date: $cell => Date.parse(`1 ${$cell.text()}`),
  alpha: $cell => $.trim(
    $cell.find('span.sort-key').text() + ' ' +
    $cell.text().toUpperCase()
  ),
  numeric($cell) {
    const key = parseFloat(
      $cell
        .text()
        .replace(/^[^\d.]*/, '')
    );
    return isNaN(key) ? 0 : key;
  }
};

$('#t-1')
  .find('thead th')
  .slice(1)
  .each((i, element) => {
    $(element).data(
      'keyType',
      element.className.replace(/^sort-/,'')
    );
  })
  // ...

列表 12.5

我们已修改脚本,为每个列头单元格存储基于其类名的keyType数据。我们去掉类名的sort-部分,这样就剩下alphanumericdate。通过将每个排序函数作为sortKeys对象的方法,我们可以使用数组表示法,并传递表头单元格的keyType数据的值来调用适当的函数。

通常,当我们调用方法时,我们使用点符号。事实上,在本书中,我们调用 jQuery 对象的方法就是这样的。例如,要向<div class="foo">添加一个bar类,我们写$('div.foo').addClass('bar')。因为 JavaScript 允许以点符号或数组符号表示属性和方法,所以我们也可以写成$('div.foo')'addClass'。大多数情况下这样做没有太多意义,但这可以是一种有条件地调用方法而不使用一堆if语句的好方法。对于我们的sortKeys对象,我们可以像这样调用alpha方法sortKeys.alpha($cell)sortKeys'alpha'或者,如果方法名存储在一个keyType常量中,sortKeyskeyType。我们将在click处理程序内使用这种第三种变体:

// ...
.on('click', (e) => {
  e.preventDefault();

  const column = $(e.currentTarget).index();
  const keyType = $(e.currentTarget).data('keyType');

  $('#t-1')
    .find('tbody > tr')
    .each((i, element) => {
      $(element).data(
        'sortKey',
        sortKeyskeyType
            .children('td')
            .eq(column)
        )
      );
    })
    .get()
    .sort((a, b) => comparator(
      $(a).data('sortKey'),
      $(b).data('sortKey')
    ))
    .forEach((element) => {
      $(element)
        .parent()
        .append(element);
    });
}); 

列表 12.6

现在我们也可以按发布日期或价格排序:

交替排序方向

我们的最终排序增强是允许升序降序排序顺序。当用户点击已经排序的列时,我们希望反转当前的排序顺序。

要反转排序,我们只需反转比较器返回的值。我们可以通过简单的direction参数来做到这一点:

const comparator = (a, b, direction = 1) =>
  a < b ?
    -direction :
    (a > b ? direction : 0);

如果direction等于1,那么排序将与之前相同。如果它等于-1,则排序将被反转。通过将这个概念与一些类结合起来以跟踪列的当前排序顺序,实现交替排序方向就变得简单了:

// ...
.on('click', (e) => {
  e.preventDefault();

  const $target = $(e.currentTarget);
  const column = $target.index();
  const keyType = $target.data('keyType');
  const sortDirection = $target.hasClass('sorted-asc') ?
    -1 : 1;

  $('#t-1')
    .find('tbody > tr')
    .each((i, element) => {
      $(element).data(
        'sortKey',
        sortKeyskeyType
            .children('td')
            .eq(column)
        )
      );
    })
    .get()
    .sort((a, b) => comparator(
      $(a).data('sortKey'),
      $(b).data('sortKey'),
      sortDirection
    ))
    .forEach((element) => {
      $(element)
        .parent()
        .append(element);
    });

    $target
      .siblings()
      .addBack()
      .removeClass('sorted-asc sorted-desc')
      .end()
      .end()
      .addClass(
        sortDirection == 1 ?
          'sorted-asc' : 'sorted-desc'
      );
}); 

列表 12.7

作为一个额外的好处,由于我们使用类来存储排序方向,我们可以将列标题样式化以指示当前顺序:

使用 HTML5 自定义数据属性

到目前为止,我们一直依赖表格单元格内的内容来确定排序顺序。虽然我们已经通过操作内容来正确排序行,但我们可以通过以HTML5 数据属性的形式从服务器输出更多的 HTML 来使我们的代码更高效。我们示例页面中的第二个表格包含了这些属性:

<table id="t-2" class="sortable"> 
  <thead> 
    <tr> 
      <th></th> 
      <th data-sort='{"key":"title"}'>Title</th> 
      <th data-sort='{"key":"authors"}'>Author(s)</th> 
      <th data-sort='{"key":"publishedYM"}'>Publish Date</th> 
      <th data-sort='{"key":"price"}'>Price</th> 
    </tr> 
  </thead> 
  <tbody> 
    <tr data-book='{"img":"2862_OS.jpg", 
      "title":"DRUPAL 7","authors":"MERCER DAVID",       
      "published":"September 2010","price":44.99,       
      "publishedYM":"2010-09"}'> 
      <td><img src="img/2862_OS.jpg" alt="Drupal 7"></td> 
      <td>Drupal 7</td> 
      <td>David Mercer</td> 
      <td>September 2010</td> 
      <td>$44.99</td> 
    </tr> 
    <!-- code continues --> 
  </tbody> 
</table> 

请注意,每个<th>元素(除了第一个)都有一个data-sort属性,每个<tr>元素都有一个data-book属性。我们在第七章中首次看到自定义数据属性,使用插件,在那里我们提供了插件代码使用的属性信息。在这里,我们将使用 jQuery 自己来访问属性值。要检索值,我们将data-后的属性名部分传递给.data()方法。例如,我们写$('th').first().data('sort')来获取第一个<th>元素的data-sort属性的值。

当我们使用 .data() 方法获取数据属性的值时,如果 jQuery 确定它是其中一种类型,它会将值转换为数字、数组、对象、布尔值或 null。对象必须使用 JSON 语法表示,就像我们在这里做的一样。因为 JSON 格式要求其键和字符串值使用双引号括起来,所以我们需要使用单引号来包围属性值:

<th data-sort='{"key":"title"}'> 

由于 jQuery 会将 JSON 字符串转换为对象,因此我们可以简单地获取我们想要的值。例如,要获取key属性的值,我们写:

$('th').first().data('sort').key 

一旦以这种方式检索了自定义数据属性,数据就被 jQuery 内部存储起来,HTML data-* 属性本身不再被访问或修改。

在这里使用数据属性的一个很大的好处是,存储的值可以与表格单元格内容不同。换句话说,我们在第一个表格中必须做的所有工作以调整排序--将字符串转换为大写,更改日期格式,将价格转换为数字--已经处理过了。这使我们能够编写更简单、更高效的排序代码:

$(() => {
  const comparator = (a, b, direction = 1) =>
    a < b ?
      -direction :
      (a > b ? direction : 0);

  $('#t-2')
    .find('thead th')
    .slice(1)
    .wrapInner($('<a/>').attr('href', '#'))
    .addClass('sort')
    .on('click', (e) => {
      e.preventDefault();

      const $target = $(e.currentTarget);
      const column = $target.index();
      const sortKey = $target.data('sort').key;
      const sortDirection = $target.hasClass('sorted-asc') ?
        -1 : 1;

      $('#t-2')
        .find('tbody > tr')
        .get()
        .sort((a, b) => comparator(
          $(a).data('book')[sortKey],
          $(b).data('book')[sortKey],
          sortDirection
        ))
        .forEach((element) => {
          $(element)
            .parent()
            .append(element);
        });

      $target
        .siblings()
        .addBack()
        .removeClass('sorted-asc sorted-desc')
        .end()
        .end()
        .addClass(
          sortDirection == 1 ?
            'sorted-asc' : 'sorted-desc'
        );
    });
}); 

第 12.8 节

这种方法的简单性是显而易见的:sortKey常量被设置为.data('sort').key,然后用它来比较行的排序值和$(a).data('book')[sortKey]以及$(b).data('book')[sortKey]。其效率表现在无需先循环遍历行,然后每次在调用sort函数之前调用sortKeys函数之一。通过这种简单和高效的结合,我们还提高了代码的性能并使其更易于维护。

使用 JSON 排序和构建行

到目前为止,在本章中,我们一直在朝着将更多信息从服务器输出到 HTML 中的方向前进,以便我们的客户端脚本尽可能保持简洁和高效。现在让我们考虑一个不同的情景,即在 JavaScript 可用时显示一整套新的信息。越来越多的 Web 应用程序依赖于 JavaScript 传递内容以及一旦内容到达后对其进行操作。在我们的第三个表格排序示例中,我们将做同样的事情。

我们将首先编写三个函数:

  • buildAuthors(): 这个函数用于构建作者名称的字符串列表。

  • buildRow(): 这个函数用于构建单个表格行的 HTML。

  • buildRows(): 这个函数通过映射buildRow()构建的行来构建整个表格的 HTML。

const buildAuthors = row =>
  row
    .authors
    .map(a => `${a.first_name} ${a.last_name}`)
    .join(', ');

const buildRow = row =>
  `
    <tr>
      <td><img src="img/${row.img}"></td>
      <td>${row.title}</td>
      <td>${buildAuthors(row)}</td>
      <td>${row.published}</td>
      <td>$${row.price}</td>
    </tr>
  `;

const buildRows = rows =>
  rows
    .map(buildRow)
    .join(''); 

第 12.9 节

对于我们的目的,我们可以使用一个函数来处理这两个任务,但是通过使用三个独立的函数,我们留下了在其他时间点构建和插入单个行的可能性。这些函数将从对 Ajax 请求的响应中获取它们的数据:

Promise.all([$.getJSON('books.json'), $.ready])
  .then(([json]) => {
    $('#t-3')
      .find('tbody')
      .html(buildRows(json));
  })
  .catch((err) => {
    console.error(err);
  }); 

第 12.10 节

在进行 Ajax 调用之前,我们不应该等待 DOM 准备就绪。在我们可以使用 JSON 数据调用buildRows()之前,有两个 promise 需要解决。首先,我们需要来自服务器的实际 JSON 数据。其次,我们需要确保 DOM 已准备好进行操作。因此,我们只需创建一个新的 promise,在这两件事发生时解决它,使用Promise.all()$.getJSON()函数返回一个 promise,而$.ready是一个在 DOM 准备就绪时解决的 promise。

还值得注意的是,我们需要以不同方式处理authors数据,因为它作为一个具有first_namelast_name属性的对象数组从服务器返回,而其他所有数据都作为字符串或数字返回。我们遍历作者数组--尽管对于大多数行,该数组只包含一个作者--并连接名字和姓氏。然后,我们使用逗号和空格将数组值连接起来,得到一个格式化的姓名列表。

buildRow()函数假设我们从 JSON 文件中获取的文本是安全可用的。由于我们将<img><td><tr>标签与文本内容连接成一个字符串,我们需要确保文本内容没有未转义的<>&字符。确保 HTML 安全字符串的一种方法是在服务器上处理它们,将所有的<转换为&lt;>转换为&gt;,并将&转换为&amp;

修改 JSON 对象

我们对authors数组的处理很好,如果我们只计划调用buildRows()函数一次的话。然而,由于我们打算每次对行进行排序时都调用它,提前格式化作者信息是个好主意。趁机我们也可以对标题和作者信息进行排序格式化。与第二个表格不同的是,第三个表格检索到的 JSON 数据只有一种类型。但是,通过编写一个额外的函数,我们可以在到达构建表格函数之前包含修改后的排序和显示值:

const buildAuthors = (row, separator = ', ') =>
  row
    .authors
    .map(a => `${a.first_name} ${a.last_name}`)
    .join(separator);

const prepRows = rows =>
  rows
    .map(row => $.extend({}, row, {
      title: row.title.toUpperCase(),
      titleFormatted: row.title,
      authors: buildAuthors(row, ' ').toUpperCase(),
      authorsFormatted: buildAuthors(row)
    }));

列表 12.11

通过将我们的 JSON 数据传递给这个函数,我们为每一行的对象添加了两个属性:authorsFormattedtitleFormatted。这些属性将用于显示的表格内容,保留原始的authorstitle属性用于排序。用于排序的属性也转换为大写,使排序操作不区分大小写。我们还在buildAuthors()函数中添加了一个新的分隔符参数,以便在这里使用它。

当我们立即在 $.getJSON() 回调函数内调用这个 prepRows() 函数时,我们将修改后的 JSON 对象的返回值存储在 rows 变量中,并将其用于排序和构建。这意味着我们还需要改变 buildRow() 函数以利用我们提前准备的简便性:

const buildRow = row =>
  `
    <tr>
      <td><img src="img/${row.img}"></td>
      <td>${row.titleFormatted}</td>
      <td>${row.authorsFormatted}</td>
      <td>${row.published}</td>
      <td>$${row.price}</td>
    </tr>
  `;

Promise.all([$.getJSON('books.json'), $.ready])
  .then(([json]) => {
    $('#t-3')
      .find('tbody')
      .html(buildRows(prepRows(json)));
  })
  .catch((err) => {
    console.error(err);
  });

清单 12.12

根据需要重建内容

现在,我们已经为排序和显示准备好了内容,我们可以再次实现列标题修改和排序例程:

Promise.all([$.getJSON('books.json'), $.ready])
  .then(([json]) => {
    $('#t-3')
      .find('tbody')
      .html(buildRows(prepRows(json)));

    const comparator = (a, b, direction = 1) =>
      a < b ?
        -direction :
        (a > b ? direction : 0);

    $('#t-3')
      .find('thead th')
      .slice(1)
      .wrapInner($('<a/>').attr('href', '#'))
      .addClass('sort')
      .on('click', (e) => {
        e.preventDefault();

        const $target = $(e.currentTarget);
        const column = $target.index();
        const sortKey = $target.data('sort').key;
        const sortDirection = $target.hasClass('sorted-asc') ?
          -1 : 1;
        const content = buildRows(
          prepRows(json).sort((a, b) => comparator(
            a[sortKey],
            b[sortKey],
            sortDirection
          ))
        );

        $('#t-3')
          .find('tbody')
          .html(content);

        $target
          .siblings()
          .addBack()
          .removeClass('sorted-asc sorted-desc')
          .end()
          .end()
          .addClass(
            sortDirection == 1 ?
              'sorted-asc' : 'sorted-desc'
          );
      });
})
.catch((err) => {
  console.error(err);
}); 

清单 12.13

click 处理程序中的代码与清单 12.8中第二个表格的处理程序几乎相同。唯一显著的区别是,这里我们每次排序只向 DOM 中插入一次元素。在表格一和表格二中,即使经过其他优化,我们也是对实际的 DOM 元素进行排序,然后逐个循环遍历它们,将每一个依次附加以达到新的顺序。例如,在清单 12.8中,表格行是通过循环重新插入的:

.forEach((element) => {
  $(element)
    .parent()
    .append(element);
}); 

这种重复的 DOM 插入在性能上可能是相当昂贵的,特别是当行数很大时。与我们在清单 12.13中的最新方法相比:

$('#t-3')
  .find('tbody')
  .html(content);

buildRows() 函数返回表示行的 HTML 字符串,并一次性插入,而不是移动现有行。

重新审视属性操作

到现在,我们已经习惯于获取和设置与 DOM 元素相关的值。我们使用了简单的方法,例如 .attr().prop().css(),方便的快捷方式,例如 .addClass().css().val(),以及复杂的行为捆绑,例如 .animate()。即使是简单的方法,它们也在幕后为我们做了很多工作。如果我们更好地理解它们的工作原理,我们可以更有效地利用它们。

使用简写元素创建语法

我们经常通过将 HTML 字符串提供给 $() 函数或 DOM 插入函数来在我们的 jQuery 代码中创建新元素。例如,我们在清单 12.9中创建一个大的 HTML 片段以产生许多 DOM 元素。这种技术快速而简洁。在某些情况下,它并不理想。例如,我们可能希望在使用文本之前对特殊字符进行转义,或者应用浏览器相关的样式规则。在这些情况下,我们可以创建元素,然后链式附加额外的 jQuery 方法来修改它,就像我们已经做过很多次一样。除了这种标准技术之外,$() 函数本身提供了一种实现相同结果的替代语法。

假设我们想在文档中的每个表格之前引入标题。我们可以使用 .each() 循环来遍历表格并创建一个适当命名的标题:

$(() => {
  $('table')
    .each((i, table) => {
      $('<h3/>', {
        'class': 'table-title',
        id: `table-title-${i}`,
        text: `Table ${i + 1}`,
        data: { index: i },
        click(e) {
          e.preventDefault();
          $(table).fadeToggle();
        },
        css: { glowColor: '#00ff00', cursor: 'pointer' }
      }).insertBefore(table);
    });
}); 

清单 12.14

将选项对象作为第二个参数传递给 $() 函数与首先创建元素然后将该对象传递给 .attr() 方法具有相同的效果。正如我们所知,这个方法让我们设置 DOM 属性,如元素的 id 值和其 class

我们示例中的其他选项包括:

  • 元素内的文本

  • 自定义额外数据

  • 点击处理程序

  • 包含 CSS 属性的对象

这些不是 DOM 属性,但它们仍然被设置。简写的 $() 语法能够处理这些,因为它首先检查给定名称的 jQuery 方法是否存在,如果存在,则调用它而不是设置该名称的属性。

因为 jQuery 会将方法优先于属性名称,所以在可能产生歧义的情况下,我们必须小心;例如,<input> 元素的 size 属性,因为存在 .size() 方法,所以不能以这种方式设置。

这个简写的 $() 语法,连同 .attr() 函数,通过使用钩子可以处理更多功能。

DOM 操作钩子

许多 jQuery 方法可以通过定义适当的钩子来扩展特殊情况下的获取和设置属性。这些钩子是在 jQuery 命名空间中的数组,名称如 $.cssHooks$.attrHooks。通常,钩子是包含一个 get 方法以检索请求的值和一个 set 方法以提供新值的对象。

钩子类型包括:

钩子类型 修改的方法 示例用法
$.attrHooks .attr() 阻止更改元素的 type 属性。
$.cssHooks .css() 为 Internet Explorer 提供 opacity 的特殊处理。
$.propHooks .prop() 修正了 Safari 中 selected 属性的行为。
$.valHooks .val() 允许单选按钮和复选框在各个浏览器中报告一致的值。

通常这些钩子执行的工作对我们完全隐藏,我们可以从中受益而不用考虑正在发生什么。不过,有时候,我们可能希望通过添加自己的钩子来扩展 jQuery 方法的行为。

编写 CSS 钩子

列表 12.14 中的代码将一个名为 glowColor 的 CSS 属性注入到页面中。目前,这对页面没有任何影响,因为这样的属性并不存在。相反,我们将扩展 $.cssHooks 以支持这个新发明的属性。当在元素上设置 glowColor 时,我们将使用 CSS3 的 text-shadow 属性在文本周围添加柔和的辉光:

(($) => {
  $.cssHooks.glowColor = {
    set(elem, value) {
      elem.style.textShadow = value == 'none' ?
        '' : `0 0 2px ${value}`;
    }
  };
})(jQuery);

列表 12.15

钩子由元素的 get 方法和 set 方法组成。为了尽可能简洁和简单,我们目前只定义了 set

有了这个钩子,现在我们在标题文本周围有一个 2 像素的柔和绿色辉光:

虽然新的钩子按照广告展示的效果工作,但它缺少许多我们可能期望的功能。其中一些缺点包括:

  • 辉光的大小不可定制

  • 这个效果与 text-shadowfilter 的其他用法是互斥的

  • get 回调未实现,所以我们无法测试属性的当前值

  • 该属性无法进行动画处理

只要付出足够的工作和额外的代码,我们就能克服所有这些障碍。然而,在实践中,我们很少需要定义自己的钩子;有经验的插件开发人员已经为各种需要创建了钩子,包括大多数 CSS3 属性。

寻找钩子

插件的形势变化很快,所以新的钩子会不断出现,我们无法希望在这里列出所有的钩子。要了解可能的一些内容,请参阅 Brandon Aaron 的 CSS 钩子集合。

github.com/brandonaaron/jquery-cssHooks

总结

在本章中,我们用三种不同的方式解决了一个常见问题--对数据表进行排序--并比较了每种方法的优点。这样做的过程中,我们练习了我们之前学到的 DOM 修改技术,并探索了 .data() 方法,用于获取和设置与任何 DOM 元素相关联的数据,或者使用 HTML5 数据属性附加。我们还揭开了几个 DOM 修改例程的面纱,学习了如何为我们自己的目的扩展它们。

进一步阅读

本书的 附录 C 中提供了完整的 DOM 操作方法列表,或者在官方 jQuery 文档中查看 api.jquery.com/

练习

挑战性练习可能需要使用官方 jQuery 文档 api.jquery.com/

  1. 修改第一个表的关键计算,使标题和作者按长度而不是字母顺序排序。

  2. 使用第二个表中的 HTML5 数据计算所有书的价格总和,并将这个总和插入到该列的标题中。

  3. 更改用于第三个表的比较器,使包含单词 jQuery 的标题首先按标题排序。

  4. 挑战:为 glowColor CSS 钩子实现 get 回调。

第十三章:高级 Ajax

许多 Web 应用程序需要频繁的网络通信。使用 jQuery,我们的网页可以与服务器交换信息,而无需在浏览器中加载新页面。

在第六章 使用 Ajax 发送数据 中,你学会了与服务器异步交互的简单方法。在这一更高级的章节中,我们将包括:

  • 处理网络中断的错误处理技术

  • Ajax 和 jQuery 延迟对象系统之间的交互

  • 使用缓存和节流技术来减少网络流量

  • 使用传输器、预过滤器和数据类型转换器扩展 Ajax 系统的内部工作方式的方法

使用 Ajax 实现渐进增强

在整本书中,我们遇到了 渐进增强 的概念。重申一下,这一理念确保所有用户都能获得积极的用户体验,要先确保有一个可用的产品,然后再为使用现代浏览器的用户添加额外的装饰。

举例来说,我们将构建一个搜索 GitHub 代码库的表单:

<form id="ajax-form" action="https://github.com/search" method="get"> 
  <fieldset> 
    <div class="text"> 
      <label for="title">Search</label> 
      <input type="text" id="title" name="q"> 
    </div> 

    <div class="actions"> 
      <button type="submit">Request</button> 
    </div> 
  </fieldset> 
</form> 

获取示例代码

你可以从以下 GitHub 代码库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3

搜索表单是一个普通的表单元素,包括一个文本输入和一个标有请求的提交按钮:

当点击该表单的请求按钮时,表单会像平常一样提交;用户的浏览器会被重定向到github.com/search,并显示结果:

然而,我们希望将这些内容加载到我们搜索页面的 #response 容器中,而不是离开页面。如果数据存储在与我们的搜索表单相同的服务器上,我们可以使用 .load() 方法提取页面的相关部分:

$(() => {
  $('#ajax-form')
    .on('submit', (e) => {
      e.preventDefault();
      $('#response')
        .load(
          'https://github.com/search .container',
          $(e.target).serialize()
        );
    });
});

列表 13.1

然而,由于 GitHub 在不同的主机名下,浏览器的默认跨域策略将阻止这个请求的发生。

获取 JSONP 数据

在第六章 使用 Ajax 发送数据 中,我们看到 JSONP 只是 JSON 加上了允许从不同站点进行请求的服务器行为的一个附加层。当请求 JSONP 数据时,提供了一个特殊的查询字符串参数,允许请求脚本处理数据。这个参数可以被 JSONP 服务器命名任何名称;在 GitHub API 的情况下,该参数使用默认名称 callback

因为使用了默认的 callback 名称,使得要进行 JSONP 请求唯一需要的设置就是告诉 jQuery jsonp 是我们期望的数据类型:

$(() => {
  $('#ajax-form')
    .on('submit', (e) => {
      e.preventDefault();

      $.ajax({
        url: 'https://api.github.com/search/repositories',
        dataType: 'jsonp',
        data: { q: $('#title').val() },
        success(data) {
          console.log(data);
        }
      });
    });
}); 

列表 13.2

现在,我们可以在控制台中检查 JSON 数据。在这种情况下,数据是一个对象数组,每个对象描述一个 GitHub 代码库:

{
  "id": 167174,
  "name": "jquery",
  "open_issues": 78,
  "open_issues_count": 78,
  "pulls_url: "https://api.github.com/repos/jquery/jquery/pulls{/number}",
  "pushed_at": "2017-03-27T15:50:12Z",
  "releases_url": "https://api.github.com/repos/jquery/jquery/releases{/id}",
  "score": 138.81496,
  "size": 27250,
  "ssh_url": "[email protected]:jquery/jquery.git",
  "stargazers_count": 44069,
  "updated_at": "2017-03-27T20:59:42Z",
  "url": "https://api.github.com/repos/jquery/jquery",
  "watchers": 44069,
  // ...
} 

关于一个仓库的所有我们需要显示的数据都包含在这个对象中。我们只需要适当地对其进行格式化以进行显示。为一个项目创建 HTML 有点复杂,所以我们将这一步拆分成自己的辅助函数:

const buildItem = item =>
  `
    <li>
      <h3><a href="${item.html_url}">${item.name}</a></h3>
      <div>★ ${item.stargazers_count}</div>
      <div>${item.description}</div>
    </li>
  `;

第 13.3 节

buildItem()函数将 JSON 对象转换为 HTML 列表项。这包括一个指向主 GitHub 仓库页面的链接,后跟描述。

在这一点上,我们有一个函数来为单个项目创建 HTML。当我们的 Ajax 调用完成时,我们需要在每个返回的对象上调用此函数,并显示所有结果:

$(() => {
  $('#ajax-form')
    .on('submit', (e) => {
      e.preventDefault();

      $.ajax({
        url: 'https://api.github.com/search/repositories',
        dataType: 'jsonp',
        data: { q: $('#title').val() },
        success(json) {
          var output = json.data.items.map(buildItem);
          output = output.length ?
          output.join('') : 'no results found';

          $('#response').html(`<ol>${output}</ol>`);
        }
      });
    });
}); 

第 13.4 节

现在我们有一个功能性的success处理程序,在搜索时,会将结果很好地显示在我们表单旁边的一列中:

处理 Ajax 错误

将任何类型的网络交互引入应用程序都会带来一定程度的不确定性。用户的连接可能会在操作过程中断开,或者临时服务器问题可能会中断通信。由于这些可靠性问题,我们应该始终为最坏的情况做准备,并准备好处理错误情况。

$.ajax()函数可以接受一个名为error的回调函数,在这些情况下调用。在这个回调中,我们应该向用户提供某种反馈,指示发生了错误:

$(() => {
  $('#ajax-form')
    .on('submit', (e) => {
      e.preventDefault();

      $.ajax({
        url: 'https://api.github.com/search/repositories',
        dataType: 'jsonp',
        data: { q: $('#title').val() },
        error() {
          $('#response').html('Oops. Something went wrong...');
        }
      });
    });
}); 

第 13.5 节

错误回调可能由多种原因触发。其中包括:

  • 服务器返回了错误状态码,例如 403 Forbidden、404 Not Found 或 500 Internal Server Error。

  • 服务器返回了重定向状态码,例如 301 Moved Permanently。一个例外是 304 Not Modified,它不会触发错误,因为浏览器可以正确处理这种情况。

  • 服务器返回的数据无法按照指定的方式解析(例如,在dataTypejson时,它不是有效的 JSON 数据)。

  • XMLHttpRequest对象上调用了.abort()方法。

检测和响应这些条件对提供最佳用户体验非常重要。我们在第六章中看到,通过 Ajax 发送数据,如果有的话,错误代码是通过传递给错误回调的jqXHR对象的.status属性提供给我们的。如果合适的话,我们可以使用jqXHR.status的值对不同类型的错误做出不同的反应。

然而,服务器错误只有在实际观察到时才有用。有些错误会立即被检测到,但其他情况可能导致请求和最终错误响应之间的长时间延迟。

当可靠的服务器超时机制不可用时,我们可以强制执行自己的客户端请求超时。通过向超时选项提供以毫秒为单位的时间,我们告诉$.ajax()在收到响应之前超过该时间量时自行触发.abort()

$.ajax({
  url: 'https://api.github.com/search/repositories',
  dataType: 'jsonp',
  data: { q: $('#title').val() },
  timeout: 10000,
  error() {
    $('#response').html('Oops. Something went wrong...');
  }
});

第 13.6 节

有了超时设置,我们可以确保在 10 秒内要么加载数据,要么用户会收到错误消息。

使用 jqXHR 对象

当发出 Ajax 请求时,jQuery 会确定获取数据的最佳机制。这个传输可以是标准的XMLHttpRequest对象,Microsoft ActiveX 的XMLHTTP对象或者<script>标签。

因为使用的传输方式可能会因请求而异,所以我们需要一个通用接口来与通信进行交互。jqXHR对象为我们提供了这个接口。当使用该传输方式时,它是XMLHttpRequest对象的包装器,在其他情况下,它会尽可能模拟XMLHttpRequest。它暴露的属性和方法包括:

  • .responseText.responseXML,包含返回的数据

  • .status.statusText,包含状态代码和描述

  • .setRequestHeader()以操作与请求一起发送的 HTTP 头部。

  • .abort()以过早终止事务

所有 jQuery 的 Ajax 方法都会返回这个jqXHR对象,因此,如果我们需要访问这些属性或方法,我们可以存储结果。

Ajax promises

然而,比XMLHttpRequest接口更重要的是,jqXHR还充当了一个 promise。在第十一章的高级特效中,你了解了 deferred 对象,它允许我们设置在某些操作完成时触发回调。Ajax 调用就是这样一种操作的示例,jqXHR对象提供了我们从 deferred 对象的 promise 中期望的方法。

使用 promise 的方法,我们可以重写我们的$.ajax()调用,以替换成功和错误回调的替代语法:

$.ajax({
  url: 'https://api.github.com/search/repositories',
  dataType: 'jsonp',
  data: { q: $('#title').val() },
  timeout: 10000,
}).then((json) => {
  var output = json.data.items.map(buildItem);
  output = output.length ?
    output.join('') : 'no results found';

  $('#response').html(`<ol>${output}</ol>`);
}).catch(() => {
  $('#response').html('Oops. Something went wrong...');
});

列表 13.7

乍一看,调用.then().catch()似乎并不比我们之前使用的回调语法更有用。然而,promise 方法提供了几个优点。首先,这些方法可以被多次调用以添加更多的处理程序(handlers)(如果需要的话)。其次,如果我们将$.ajax()调用的结果存储在一个常量中,我们可以稍后调用处理程序,如果这样做能够使我们的代码结构更易读。第三,如果在附加处理程序时 Ajax 操作已经完成,处理程序将立即被调用。最后,我们不应忽视使用与 jQuery 库其他部分和本机 JavaScript promises 一致的语法的可读性优势。

另一个使用 promise 方法的例子,我们可以在发出请求时添加一个加载指示器。由于我们希望在请求完成时隐藏指示器,无论成功与否,.always()方法将非常有用:

$('#ajax-form')
  .on('submit', (e) => {
    e.preventDefault();

    $('#response')
      .addClass('loading')
      .empty();

    $.ajax({
      url: 'https://api.github.com/search/repositories',
      dataType: 'jsonp',
      data: { q: $('#title').val() },
      timeout: 10000,
    }).then((json) => {
      var output = json.data.items.map(buildItem);
      output = output.length ?
      output.join('') : 'no results found';

      $('#response').html(`<ol>${output}</ol>`);
    }).catch(() => {
      $('#response').html('Oops. Something went wrong...');
    }).always(() => {
      $('#response').removeClass('loading');
    });
}); 

列表 13.8

在发出 $.ajax() 调用之前,我们将 loading 类添加到响应容器中。加载完成后,我们再次将其删除。通过这样做,我们进一步增强了用户体验,因为现在有一个视觉指示器表明后台正在发生某事。

要真正掌握 promise 行为如何帮助我们,我们需要看看如果将 $.ajax() 调用的结果存储起来供以后使用时我们可以做什么。

缓存响应

如果我们需要重复使用相同的数据片段,每次都进行 Ajax 请求是低效的。为了防止这种情况,我们可以将返回的数据缓存在一个变量中。当我们需要使用某些数据时,我们可以检查数据是否已经在缓存中。如果是,我们就对这些数据采取行动。如果没有,我们需要进行 Ajax 请求,在其 .done() 处理程序中,我们将数据存储在缓存中并对返回的数据进行操作。

如果我们利用 promise 的特性,事情会变得相当简单:

$(() => {
  const cache = new Map();

  $('#ajax-form')
    .on('submit', (e) => {
      e.preventDefault();

      const search = $('#title').val();

      if (search == '') {
        return;
      }

      $('#response')
        .addClass('loading')
        .empty();

      cache.set(search, cache.has(search) ?
        cache.get(search) :
        $.ajax({
          url: 'https://api.github.com/search/repositories',
          dataType: 'jsonp',
          data: { q: search },
          timeout: 10000,
        })
      ).get(search).then((json) => {
        var output = json.data.items.map(buildItem);
        output = output.length ?
          output.join('') : 'no results found';

        $('#response').html(`<ol>${output}</ol>`);
      }).catch(() => {
        $('#response').html('Oops. Something went wrong...');
      }).always(() => {
        $('#response').removeClass('loading');
      });
    });
}); 

列表 13.9

我们引入了一个名为 cache 的新的 Map 常量,用于保存我们创建的 jqXHR promises。这个映射的键对应于正在执行的搜索。当提交表单时,我们会查看是否已经为该键存储了一个 jqXHR promise。如果没有,我们像以前一样执行查询,将结果对象存储在 api 中。

.then().catch().always() 处理程序然后附加到 jqXHR promise。请注意,无论是否进行了 Ajax 请求,这都会发生。这里有两种可能的情况需要考虑。

首先,如果之前还没有发送过 Ajax 请求,就会发送 Ajax 请求。这与以前的行为完全一样:发出请求,然后我们使用 promise 方法将处理程序附加到 jqXHR 对象上。当服务器返回响应时,会触发适当的回调,并将结果打印到屏幕上。

另一方面,如果我们过去执行过此搜索,则 cache 中已经存储了 jqXHR promise。在这种情况下,不会执行新的搜索,但我们仍然在存储的对象上调用 promise 方法。这会将新的处理程序附加到对象上,但由于延迟对象已经解决,因此相关的处理程序会立即触发。

jQuery 延迟对象系统为我们处理了所有繁重的工作。几行代码,我们就消除了应用程序中的重复网络请求。

限制 Ajax 请求速率

搜索的常见功能是在用户输入时显示动态结果列表。我们可以通过将处理程序绑定到 keyup 事件来模拟这个“实时搜索”功能,用于我们的 jQuery API 搜索:

$('#title')
  .on('keyup', (e) => {
    $(e.target.form).triggerHandler('submit');
  });

列表 13.10

在这里,我们只需在用户在搜索字段中键入任何内容时触发表单的提交处理程序。这可能导致快速连续发送许多请求到网络,这取决于用户输入的速度。这种行为可能会降低 JavaScript 的性能;它可能会堵塞网络连接,而服务器可能无法处理这种需求。

我们已经通过刚刚实施的请求缓存来限制请求的数量。然而,我们可以通过对请求进行限速来进一步减轻服务器的负担。在第十章中,高级事件,我们介绍了当我们创建一个特殊的 throttledScroll 事件以减少原生滚动事件触发的次数时,引入了节流的概念。在这种情况下,我们希望类似地减少活动; 这次是使用 keyup 事件:

const searchDelay = 300;
var searchTimeout;

$('#title')
  .on('keyup', (e) => {
    clearTimeout(searchTimeout);

    searchTimeout = setTimeout(() => {
      $(e.target.form).triggerHandler('submit');
    }, searchDelay);
  });

列表 13.11

我们在这里使用的技术有时被称为防抖动,与我们在第十章中使用的技术有所不同。在那个例子中,我们需要我们的 scroll 处理程序在滚动继续时多次生效,而在这里,我们只需要在输入停止后一次发生 keyup 行为。为了实现这一点,我们跟踪一个 JavaScript 计时器,该计时器在用户按键时启动。每次按键都会重置该计时器,因此只有当用户停止输入指定的时间(300 毫秒)后,submit 处理程序才会被触发,然后执行 Ajax 请求。

扩展 Ajax 功能

jQuery Ajax 框架是强大的,正如我们所见,但即使如此,有时我们可能想要改变它的行为方式。毫不奇怪,它提供了多个钩子,可以被插件使用,为框架提供全新的功能。

数据类型转换器

在第六章中,使用 Ajax 发送数据,我们看到 $.ajaxSetup() 函数允许我们更改 $.ajax() 使用的默认值,从而可能影响许多 Ajax 操作只需一次语句。这个相同的函数也可以用于扩展 $.ajax() 可以请求和解释的数据类型范围。

举个例子,我们可以添加一个理解 YAML 数据格式的转换器。YAML(www.yaml.org/)是一种流行的数据表示,许多编程语言都有实现。如果我们的代码需要与这样的替代格式交互,jQuery 允许我们将其兼容性构建到本地 Ajax 函数中。

包含 GitHub 仓库搜索条件的简单 YAML 文件:

Language:
 - JavaScript
 - HTML
 - CSS
Star Count:
 - 5000+
 - 10000+
 - 20000+

我们可以将 jQuery 与现有的 YAML 解析器(如 Diogo Costa 的 code.google.com/p/javascript-yaml-parser/)结合起来,使 $.ajax() 也能够使用这种语言。

定义一个新的 Ajax 数据类型涉及将三个属性传递给$.ajaxSetup()acceptscontentsconvertersaccepts属性添加要发送到服务器的头,声明服务器理解我们的脚本的特定 MIME 类型。contents属性处理交易的另一侧,提供一个与响应 MIME 类型匹配的正则表达式,尝试从此元数据中自动检测数据类型。最后,converters包含解析返回数据的实际函数:

$.ajaxSetup({ 
  accepts: { 
    yaml: 'application/x-yaml, text/yaml' 
  }, 
  contents: { 
    yaml: /yaml/ 
  }, 
  converters: { 
    'text yaml': (textValue) => { 
      console.log(textValue); 
      return ''; 
    } 
  } 
}); 

$.ajax({ 
  url: 'categories.yml', 
  dataType: 'yaml' 
}); 

列表 13.12

列表 13.12中的部分实现使用$.ajax()来读取 YAML 文件,并将其数据类型声明为yaml。因为传入的数据被解析为text,jQuery 需要一种方法将一个数据类型转换为另一个。'text yaml'converters键告诉 jQuery,此转换函数将接受作为text接收的数据,并将其重新解释为yaml

在转换函数内部,我们只是记录文本内容以确保函数被正确调用。要执行转换,我们需要加载第三方 YAML 解析库(yaml.js)并调用其方法:

$.ajaxSetup({
  accepts: {
    yaml: 'application/x-yaml, text/yaml'
  },
  contents: {
    yaml: /yaml/
  },
  converters: {
    'text yaml': (textValue) => YAML.eval(textValue)
  }
});

Promise.all([
  $.getScript('yaml.js')
    .then(() =>
      $.ajax({
        url: 'categories.yml',
        dataType: 'yaml'
      })),
  $.ready
]).then(([data]) => {
  const output = Object.keys(data).reduce((result, key) =>
    result.concat(
      `<li><strong>${key}</strong></li>`,
      data[key].map(i => `<li> <a href="#">${i}</a></li>`)
    ),
    []
  ).join('');

  $('#categories')
    .removeClass('hide')
    .html(`<ul>${output}</ul>`);
}); 

列表 13.13

yaml.js文件包含一个名为YAML的对象,带有一个.eval()方法。我们使用这个方法来解析传入的文本并返回结果,这是一个包含categories.yml文件所有数据的 JavaScript 对象,以便轻松遍历结构。由于我们正在加载的文件包含 GitHub 仓库搜索字段,我们使用解析后的结构打印出顶级字段,稍后将允许用户通过点击它们来过滤其搜索结果:

Ajax 操作可能会立即运行,而无需访问 DOM,但一旦我们从中获得结果,我们需要等待 DOM 可用才能继续。将代码结构化为使用Promise.all()允许尽早执行网络调用,提高用户对页面加载时间的感知。

接下来,我们需要处理类别链接的点击:

$(document)
  .on('click', '#categories a', (e) => {
    e.preventDefault();

    $(e.target)
      .parent()
      .toggleClass('active')
      .siblings('.active')
      .removeClass('active');
    $('#ajax-form')
      .triggerHandler('submit');
  }); 

列表 13.14

通过将我们的click处理程序绑定到document并依赖事件委托,我们避免了一些昂贵的重复工作,而且我们也可以立即运行代码,而不必担心等待 Ajax 调用完成。

在处理程序中,我们确保正确的类别被突出显示,然后触发表单上的submit处理程序。我们还没有让表单理解我们的类别列表,但高亮显示已经起作用:

最后,我们需要更新表单的submit处理程序以尊重活动类别(如果有的话):

$('#ajax-form')
  .on('submit', (e) => {
    e.preventDefault();

    const search = [
      $('#title').val(),
      new Map([
        ['JavaScript', 'language:"JavaScript"'],
        ['HTML', 'language:"HTML"'],
        ['CSS', 'language:"CSS"'],
        ['5000+', 'stars:">=5000"'],
        ['10000+', 'stars:">=10000"'],
        ['20000+', 'stars:">=20000"'],
        ['', '']
      ]).get($.trim(
        $('#categories')
          .find('li.active')
          .text()
      ))
    ].join('');

    if (search == '' && category == '') {
      return;
    }

    $('#response')
      .addClass('loading')
      .empty();

    cache.set(search, cache.has(search) ?
      cache.get(search) :
      $.ajax({
        url: 'https://api.github.com/search/repositories',
        dataType: 'jsonp',
        data: { q: search },
        timeout: 10000,
      })).get(search).then((json) => {
        var output = json.data.items.map(buildItem);
        output = output.length ?
          output.join('') : 'no results found';

        $('#response').html(`<ol>${output}</ol>`);
      }).catch(() => {
        $('#response').html('Oops. Something went wrong...');
      }).always(() => {
        $('#response').removeClass('loading');
      });
  }); 

列表 13.15

现在,我们不仅仅获取搜索字段的值,还获取活动语言或星星数量的文本,通过 Ajax 调用传递这两个信息。我们使用Map实例将链接文本映射到适当的 GitHub API 语法。

现在,我们可以按主要语言或按星星数量查看仓库。一旦我们应用了这些过滤器,我们可以通过在搜索框中输入来进一步细化显示的内容:

每当我们需要支持 jQuery 尚未处理的新数据类型时,我们可以以类似于此 YAML 示例的方式定义它们。因此,我们可以根据我们的项目特定需求来塑造 jQuery 的 Ajax 库。

添加 Ajax 预过滤器

$.ajaxPrefilter()函数可以添加预过滤器,这是回调函数,允许我们在发送请求之前对其进行操作。预过滤器在$.ajax()更改或使用任何选项之前调用,因此它们是更改选项或对新的自定义选项进行操作的好地方。

预过滤器还可以通过简单地返回要使用的新数据类型的名称来操作请求的数据类型。在我们的 YAML 示例中,我们指定了yaml作为数据类型,因为我们不希望依赖服务器提供正确的响应 MIME 类型。但是,我们可以提供一个预过滤器,如果 URL 中包含相应的文件扩展名(.yml),则确保数据类型为yaml

$.ajaxPrefilter(({ url }) =>
  /.yml$/.test(url) ? 'yaml' : null
);

$.getScript('yaml.js')
  .then(() =>
    $.ajax({ url: 'categories.yml' })
  ); 

列表 13.16

一个简短的正则表达式测试options.url末尾是否是.yml,如果是,则将数据类型定义为yaml。有了这个预过滤器,我们用于获取 YAML 文档的 Ajax 调用不再需要明确地定义其数据类型。

定义替代传输

我们已经看到 jQuery 使用XMLHttpRequestActiveX<script>标签来适当处理 Ajax 事务。如果愿意,我们可以通过新的传输进一步扩展这个工具库。

传输是一个处理实际 Ajax 数据传输的对象。新的传输被定义为工厂函数,返回一个包含.send().abort()方法的对象。.send()方法负责发出请求,处理响应,并通过回调函数将数据发送回来。.abort()方法应立即停止请求。

自定义传输可以,例如,使用<img>元素来获取外部数据。这使得图像加载可以像其他 Ajax 请求一样处理,这有助于使我们的代码在内部更一致。创建这样一个传输所需的 JavaScript 代码有点复杂,所以我们将先看一下最终的产品,然后再讨论它的组成部分:

$.ajaxTransport('img', ({ url }) => {
  var $img, img, prop;

  return {
    send(headers, complete) {
      const callback = (success) => {
        if (success) {
          complete(200, 'OK', { img });
        } else {
          $img.remove();
          complete(404, 'Not Found');
        }
      }

      $img = $('<img>', { src: url });
      img = $img[0];
      prop = typeof img.naturalWidth === 'undefined' ?
        'width' : 'naturalWidth';

      if (img.complete) {
        callback(!!img[prop]);
      } else {
        $img.on('load error', ({ type }) => {
          callback(type == 'load');
        });
      }
    },

    abort() {
      if ($img) {
        $img.remove();
      } 
    }
  };
}); 

列表 13.17

在定义传输时,我们首先将数据类型名称传递给$.ajaxTransport()。这告诉 jQuery 何时使用我们的传输而不是内置机制。然后,我们提供一个返回包含适当的.send().abort()方法的新传输对象的函数。

对于我们的img传输,.send()方法需要创建一个新的<img>元素,我们给它一个src属性。这个属性的值来自于 jQuery 从$.ajax()调用中传递过来的url。浏览器将通过加载引用的图像文件的<img>元素的创建做出反应,所以我们只需检测这个加载何时完成并触发完成回调。

如果我们希望处理各种浏览器和版本的图像加载完成的情况,正确检测图像加载完成就会变得棘手。在某些浏览器中,我们可以简单地将loaderror事件处理程序附加到图像元素上。但在其他浏览器中,当图像被缓存时,loaderror不会按预期触发。

我们 清单 13.17 中的代码处理了这些不同的浏览器行为,通过检查.complete.width.naturalWidth属性的值,适当地处理每个浏览器的情况。一旦我们检测到图像加载已经成功完成或失败,我们调用callback()函数,该函数反过来调用.send()传递的complete()函数。这允许$.ajax()对图像加载做出反应。

处理中止加载要简单得多。我们的.abort()方法只需通过移除已创建的<img>元素来清理send()后的情况。

接下来,我们需要编写使用新传输的$.ajax()调用:

$.ajax({
  url: 'missing.jpg',
  dataType: 'img'
}).then((img) => {
  $('<div/>', {
    id: 'picture',
    html: img
  }).appendTo('body');
}).catch((xhr, textStatus, msg) => {
  $('<div/>', {
    id: 'picture',
    html: `${textStatus}: ${msg}`
  }).appendTo('body');
}); 

清单 13.18

要使用特定的传输,$.ajax()需要给出相应的dataType值。然后,成功和失败处理程序需要考虑到传递给它们的数据类型。我们的img传输在成功时返回一个<img>DOM 元素,因此我们的.done()处理程序将使用该元素作为新创建的<div>元素的 HTML 内容,该元素将插入到文档中。

然而在这种情况下,指定的图像文件(missing.jpg)实际上不存在。我们通过适当的.catch()处理程序考虑了此种可能性,它将错误消息插入<div>,在这个<div>中原本应该放置图像:

我们可以通过引用存在的图像来纠正这个错误:

$.ajax({
  url: 'sunset.jpg',
  dataType: 'img'
}).then((img) => {
  $('<div/>', {
    id: 'picture',
    html: img
  }).appendTo('body');
}).catch((xhr, textStatus, msg) => {
  $('<div/>', {
    id: 'picture',
    html: `${textStatus}: ${msg}`
  }).appendTo('body');
}); 

清单 13.19

现在,我们的传输已成功加载图像,我们在页面上看到了这个结果:

创建新传输是不常见的,但即使在这种情况下,jQuery 的 Ajax 功能也可以满足我们的需求。例如,将图像加载视为一个 promise 的能力意味着我们可以使用这个 Ajax 调用来与其他异步行为同步,使用Promise.all()

总结

在本章的最后,我们深入了解了 jQuery 的 Ajax 框架。现在我们可以在单个页面上打造无缝的用户体验,在需要时获取外部资源,并且注意到错误处理、缓存和节流的相关问题。我们探讨了 Ajax 框架的内部运作细节,包括 promises,transports,prefilters 和 converters。你还学会了如何扩展这些机制来满足我们脚本的需求。

进一步阅读

完整的Ajax 方法列表可以在本书的 附录 B 快速参考 中找到,或者在官方 jQuery 文档 api.jquery.com/ 上找到。

练习

挑战练习可能需要使用官方 jQuery 文档 api.jquery.com/

  1. 修改buildItem()函数,使其包含每个 jQuery 方法的长描述。

  2. 这里有一个挑战给你。向页面添加指向 Flickr 公共照片搜索(www.flickr.com/search/)的表单,并确保它具有<input name="q">和一个提交按钮。使用渐进增强从 Flickr 的 JSONP 反馈服务 api.flickr.com/services/feeds/photos_public.gne 检索照片,然后将它们插入页面的内容区域。向这个服务发送数据时,使用tags而不是q,并将format设置为json。还要注意,该服务希望 JSONP 回调名称为jsoncallback,而不是callback

  3. 这里有另一个挑战给你。在 Flickr 请求产生parsererror时为其添加错误处理。通过将 JSONP 回调名称设置回callback来测试它。

附录 A:使用 QUnit 测试 JavaScript

在本书中,我们写了很多 JavaScript 代码,我们已经看到了 jQuery 如何帮助我们相对轻松地编写这些代码的许多方式。然而,每当我们添加新功能时,我们都必须额外的手动检查我们的网页,以确保一切如预期般运作。虽然这个过程对于简单的任务可能有效,但随着项目规模和复杂性的增长,手动测试可能变得相当繁琐。新的要求可能引入回归错误,破坏先前良好运作的脚本部分。很容易忽略这些与最新代码更改无关的错误,因为我们自然只测试我们刚刚完成的部分。

我们需要的是一个自动化系统来为我们运行测试。QUnit 测试框架就是这样一个系统。虽然有许多其他的测试框架,它们都有各自的好处,但我们推荐在大多数 jQuery 项目中使用 QUnit,因为它是由 jQuery 项目编写和维护的。事实上,jQuery 本身就使用 QUnit。在这个附录中,我们将介绍:

  • 如何在项目中设置 QUnit 测试框架

  • 单元测试组织以帮助代码覆盖和维护

  • 各种 QUnit 可用的测试类型

  • 保证测试可靠指示成功代码的常见实践

  • 对 QUnit 所提供的以外的其他测试类型的建议

下载 QUnit

QUnit 框架可从官方 QUnit 网站qunitjs.com/下载。在那里,我们可以找到到稳定版本的链接(当前为 2.3.0)以及开发版本(qunit-git)。这两个版本都包括一个样式表以及用于格式化测试输出的 JavaScript 文件。

设置文档

一旦我们把 QUnit 文件放好,我们就可以设置测试 HTML 文档了。在一个典型的项目中,这个文件通常会命名为 index.html,并放在与 qunit.jsqunit.css 相同的测试子文件夹中。然而,为了演示,我们将把它放在父目录中。

文档的 <head> 元素包含了一个用于 CSS 文件的 <link> 标签和用于 jQuery、QUnit、我们将进行测试的 JavaScript 文件(A.js)以及测试本身(listings/A.*.js)的 <script> 标签。<body> 标签包含了两个主要元素用于运行和显示测试结果。

要演示 QUnit,我们将使用第二章,选择元素,和第六章,使用 Ajax 发送数据中的部分内容:

<!DOCTYPE html> 
<html> 
<head> 
  <meta charset="utf-8"> 
  <title>Appendix A Tests</title> 
  <link rel="stylesheet" href="qunit.css" media="screen"> 
  <script src="img/jquery.js"></script> 
  <script src="img/qunit.js"></script> 
  <script src="img/A.js"></script> 
  <script src="img/test.js"></script> 
</head> 
<body> 
  <div id="qunit"></div> 
  <div id="qunit-fixture"> 
    <!-- Test Markup Goes Here --> 
  </div> 
</body> 
</html> 

自第二章,选择元素之后,我们要测试的代码取决于 DOM;我们希望测试标记与我们在实际页面上使用的内容匹配。我们可以简单地复制并粘贴我们在第二章中使用的 HTML 内容,这将替换<!--测试标记在这里-->注释。

组织测试

QUnit 提供两个级别的测试分组,其名称分别根据其各自的函数调用命名:QUnit.module()QUnit.test()模块就像一个将运行测试的一般类别;测试实际上是一组测试;该函数取一个回调,在其中运行所有该测试的特定单元测试。我们将通过章节主题对我们的测试进行分组,并将代码放在我们的test/test.js文件中:

QUnit.module('Selecting');

QUnit.test('Child Selector', (assert) => {
  assert.expect(0);
});

QUnit.test('Attribute Selectors', (assert) => {
  assert.expect(0);
});

QUnit.module('Ajax'); 

列表 A.1

不需要使用此测试结构设置文件,但心中有一些整体结构是很好的。除了QUnit.module()QUnit.test()分组外,我们还需要告诉测试要期望多少断言。由于我们只是在组织,我们需要告诉测试尚未有任何断言(assert.expect(0))以便进行测试。

请注意,我们的模块和测试不需要放在$(() => {})调用内,因为 QUnit 默认会等到窗口加载完毕后才开始运行测试。通过这个非常简单的设置,加载测试 HTML 会导致页面看起来像这样:

请注意,模块名称为浅蓝色,测试名称为深蓝色。单击任一将展开该组测试的结果,这些结果在通过该组的所有测试时,默认情况下是折叠的。Ajax 模块尚未出现,因为我们还没有为其编写任何测试。

添加和运行测试

测试驱动开发中,我们在编写代码之前编写测试。这样,当测试失败时,我们可以添加新代码,然后看到测试通过,验证我们的更改具有预期效果。

让我们从测试我们在第二章中使用的子选择器开始,选择元素,向所有<ul id="selected-plays">的子元素<li>添加horizontal类:

QUnit.test('Child Selector', (assert) => {
  assert.expect(1);
  const topLis = $('#selected-plays > li.horizontal');
  assert.equal(topLis.length, 3, 'Top LIs have horizontal class');
}); 

列表 A.2

我们正在测试我们选择页面上元素的能力,因此我们使用断言 assert.equal() 测试来比较顶级<li>元素的数量是否等于数字3。如果两者相等,测试成功,并添加到通过测试的数量中。如果不相等,则测试失败:

当然,测试失败了,因为我们还没有编写代码将horizontal类添加到元素中。尽管如此,添加该代码非常简单。我们在页面的主脚本文件中执行,我们将其称为A.js

$(() => { 
  $('#selected-plays > li').addClass('horizontal'); 
}); 

列表 A.3

现在运行测试时,测试如预期般通过:

现在选择:子选择器测试显示圆括号中的 1,表示总测试数为一。 现在我们可以进一步测试,通过添加一些属性选择器测试:

QUnit.module('Selecting', { 
  beforeEach() { 
    this.topLis = $('#selected-plays > li.horizontal'); 
  } 
}); 

QUnit.test('Child Selector', function(assert) { 
  assert.expect(1); 
  assert.equal(this.topLis.length, 3,  
    'Top LIs have horizontal class'); 
}); 

QUnit.test('Attribute Selectors', function(assert) { 
  assert.expect(2); 
  assert.ok(this.topLis.find('.mailto').length == 1, 'a.mailto'); 
  assert.equal(this.topLis.find('.pdflink').length, 1, 'a.pdflink'); 
}); 

A.4 清单

在这里,我们介绍了另一种类型的测试:ok()。 这个函数接受两个参数:一个表达式,如果成功则应评估为 true,以及一个描述。 还要注意,我们将本地的 topLis 变量从子选择器测试中移到了清单 A.2中,并将其放入模块的beforeEach()回调函数中。 QUnit.module() 函数接受一个可选的第二个参数,这是一个普通对象,可以包含一个 beforeEach() 和一个 afterEach() 函数。 在这些函数内部,我们可以使用this作为模块所有测试的共享上下文。

再次,如果没有相应的工作代码,新测试将失败:

在这里,我们可以看到assert.ok()测试和assert.equal()测试之间的测试失败输出的差异,assert.ok()测试仅显示测试的标签(a.mailto)和源,而assert.equal()测试还详细说明了预期的结果(而不总是期望true)。 因为它为测试失败提供了更多信息,通常优先使用assert.equal()而不是assert.ok()

让我们包含必要的代码:

$(() => { 
  $('#selected-plays > li').addClass('horizontal'); 
  $('a[href^="mailto:"]').addClass('mailto'); 
  $('a[href$=".pdf"]').addClass('pdflink'); 
}); 

A.5 清单

现在两个测试通过了,我们可以通过扩展集来看到:

在失败时,assert.equal() 提供了比assert.ok()更多的信息。 成功时,两个测试只显示标签。

异步测试

测试异步代码,如 Ajax 请求,提供了额外的挑战。 其余测试必须在异步测试发生时暂停,然后在完成时重新开始。 这种类型的场景现在非常熟悉; 我们在特效队列、Ajax 回调函数和 promise 对象中看到了这样的异步操作。 QUnit 中的异步测试与常规的 QUnit.test() 函数类似,只是它将暂停测试的运行,直到我们使用由 assert.async() 函数创建的函数调用恢复它们:

QUnit.test('JSON', (assert) => {
  assert.expect(0);
  const done = assert.async();

  $.getJSON('A.json', (json, textStatus) => {
    // add tests here
  }).always(done);
});

A.6 清单

这里我们只是从a.json请求 JSON,并且在请求完成后允许测试继续,无论成功与否,都会在.always()回调函数内调用done()。 对于实际的测试,我们将检查textStatus值以确保请求成功,并检查响应 JSON 数组中一个对象的值:

QUnit.test('JSON', (assert) => {
  const backbite = {
    term: 'BACKBITE',
    part: 'v.t.',
    definition: 'To speak of a man as you find him when he can't find you.'
  };

  assert.expect(2);
  const done = assert.async();

  $.getJSON('A.json', (json, textStatus) => {
    assert.equal(textStatus, 'success', 'Request successful');
    assert.deepEqual(
      json[1],
      backbite,
      'result array matches "backbite" map'
    );
  }).always(done);
}); 

A.7 清单

为了测试响应值,我们使用另一个测试函数:assert.deepEqual()。通常当比较两个对象时,除非它们实际上指向内存中的相同位置,否则它们被认为不相等。如果我们想要比较对象的内容,应使用 assert.deepEqual()。这个函数会遍历两个对象,确保它们具有相同的属性,并且这些属性具有相同的值。

其他类型的测试

QUnit 还配备了其他一些测试函数。其中有些函数,比如notEqual()notDeepEqual(),只是我们使用的函数的反义,而另一些函数,比如strictEqual()throws(),具有更明显的用途。有关这些函数的更多信息,以及有关 QUnit 的概述和其他示例的详细信息,可以在 QUnit 网站(qunitjs.com/)以及 QUnit API 网站(api.qunitjs.com/)上找到。

实际考虑

本附录中的示例必须是简单的。在实践中,我们可以编写确保相当复杂行为的正确操作的测试。

理想情况下,我们尽量使我们的测试尽可能简洁和简单,即使它们测试的行为很复杂。通过为一些特定的场景编写测试,我们可以相当确定地确保我们完全测试了行为,即使我们并没有针对每一种可能的输入情况编写测试。

然而,即使我们已经为其编写了测试,可能会在我们的代码中观察到一个错误。当测试通过但出现错误时,正确的响应不是立即修复问题,而是首先为失败的行为编写一个新的测试。这样,我们不仅可以在纠正代码时验证问题是否解决,还可以引入额外的测试,帮助我们避免将来出现回归问题。

QUnit 除了单元测试之外,还可以用于功能测试。单元测试旨在确认代码单元(方法和函数)的正确运行,而功能测试则旨在确保用户输入的适当接口响应。例如,在第十二章中的高级 DOM 操作中,我们实现了表格排序行为。我们可以为排序方法编写一个单元测试,验证一旦调用方法表格就排序了。另外,功能测试可以模拟用户点击表头,然后观察结果以检查表格是否确实已排序。

与 QUnit 配合使用的功能测试框架,例如 dominator.js (mwbrooks.github.io/dominator.js/) 和 FuncUnit (funcunit.com/),可以帮助更轻松地编写功能测试和模拟事件。为了在各种浏览器中进一步自动化测试,可以将 Selenium (seleniumhq.org/) 套件与这些框架一起使用。

为了确保我们的测试结果一致,我们需要使用可靠且不变的样本数据进行工作。当测试应用于动态站点的 jQuery 代码时,捕获和存储页面的静态版本以运行测试可能是有益的。这种方法还可以隔离您的代码组件,使得更容易确定错误是由服务器端代码还是浏览器端代码引起的。

进一步阅读

这些考虑肯定不是一个详尽的列表。测试驱动开发是一个深入的话题,一个简短的附录是不足以完全涵盖的。一些在线资源包含有关该主题的更多信息,包括:

这个主题也有很多书籍,比如:

  • 以示例驱动的测试, Kent Beck

  • Addison Wesley Signature Series

  • Test-Driven JavaScript Development, Christian Johansen, Addison Wesley

摘要

使用 QUnit 进行测试可以有效地帮助我们保持 jQuery 代码的清洁和可维护性。我们已经看到了一些在项目中实现测试以确保我们的代码按照我们意图的方式运行的方法。通过测试代码的小、独立单元,我们可以减轻项目变得更复杂时出现的一些问题。同时,我们可以更有效地在整个项目中测试回归,节省宝贵的编程时间。

附录 B:快速参考

本附录旨在快速参考 jQuery API,包括其选择器表达式和方法。每个方法和选择器的更详细讨论可在 jQuery 文档站点api.jquery.com上找到。

选择器表达式

jQuery 工厂函数$()用于查找页面上要处理的元素。此函数采用由类似 CSS 语法构成的字符串,称为选择器表达式。选择器表达式在第二章选择元素中有详细讨论。

简单 CSS

选择器 匹配
* 所有元素。
#id 具有给定 ID 的元素。
element 给定类型的所有元素。
.class 所有具有给定类的元素。
a, b ab匹配的元素。
a b a后代的元素b
a > b a的子元素b
a + b 紧接着a的元素b
a ~ b a兄弟且在a之后的元素b

兄弟节点位置

选择器 匹配
:nth-child(index) 是其父元素的index子元素(基于 1)。
:nth-child(even) 是其父元素的偶数子元素(基于 1)。
:nth-child(odd) 元素是其父元素的奇数子元素(基于 1)。
:nth-child(formula) 是其父元素的第 n 个子元素(基于 1)。公式的形式为an+b,其中ab为整数。
:nth-last-child() :nth-child()相同,但从最后一个元素向第一个元素计数。
:first-child 其父元素的第一个子元素。
:last-child 其父元素的最后一个子元素。
:only-child 其父元素的唯一子元素。
:nth-of-type() :nth-child()相同,但仅计算相同元素名称的元素。
:nth-last-of-type() :nth-last-child()相同,但仅计算相同元素名称的元素。
:first-of-type 是其兄弟中相同元素名称的第一个子元素。
:last-of-type 是其兄弟元素中相同元素名称的最后一个子元素。
:only-of-type() 是其兄弟中相同元素名称的唯一子元素。

匹配元素位置

选择器 匹配
:first 结果集中的第一个元素。
:last 结果集中的最后一个元素。
:not(a) 结果集中不与a匹配的所有元素。
:even 结果集中的偶数元素(基于 0)。
:odd 结果集中的奇数元素(基于 0)。
:eq(index) 结果集中的编号元素(基于 0)。
:gt(index) 结果集中给定索引(基于 0)之后的所有元素。
:lt(index) 在给定索引(基于 0)之前(小于)结果集中的所有元素。

属性

选择器 匹配
[attr] 具有attr属性的元素。
[attr="value"] attr属性为value的元素。
[attr!="value"] attr属性不是value的元素。
[attr^="value"] attr属性以value开头的元素。
[attr$="value"] attr属性以value结束的元素。
[attr*="value"] 包含子字符串valueattr属性的元素。
[attr~="value"] attr属性是一个以空格分隔的字符串集,其中之一是value的元素。
[attr&#124;="value"] attr属性等于value或以value连字符后跟的元素。

表单

选择器 匹配
:input 所有<input><select><textarea><button>元素。
:text type="text"<input>元素。
:password type="password"<input>元素。
:file type="file"<input>元素。
:radio type="radio"<input>元素。
:checkbox type="checkbox"<input>元素。
:submit type="submit"<input>元素。
:image type="image"<input>元素。
:reset type="reset"<input>元素。
:button type="button"<input>元素和<button>元素。
:enabled 启用的表单元素。
:disabled 禁用的表单元素。
:checked 已选中的复选框和单选按钮。
:selected 已选中的<option>元素。

杂项选择器

选择器 匹配
:root 文档的根元素。
:header 标题元素(例如,<h1><h2>)。
:animated 正在进行动画的元素。
:contains(text) 包含给定文本的元素。
:empty 没有子节点的元素。
:has(a) 包含匹配a的后代元素。
:parent 具有子节点的元素。
:hidden 被隐藏的元素,无论是通过 CSS 还是因为它们是<input type="hidden" />
:visible :hidden的反义。
:focus 具有键盘焦点的元素。
:lang(language) 具有给定语言代码的元素(可能是由元素或祖先上的lang属性或<meta>声明引起的)。
:target 如果有,URI 片段标识符指定的元素。

DOM 遍历方法

使用$()创建 jQuery 对象后,我们可以通过调用其中一个 DOM 遍历方法来修改我们正在处理的匹配元素集。DOM 遍历方法在第二章 选择元素中有详细讨论。

过滤

遍历方法 返回一个包含...的 jQuery 对象
.filter(selector) 匹配给定选择器的选定元素。
.filter(callback) 回调函数返回 true 的选定元素。
.eq(index) 给定基于 0 的索引处的选定元素。
.first() 第一个选定元素。
.last() 最后一个选定元素。
.slice(start, [end]) 在给定的以 0 为基础的索引范围内选择元素。
.not(selector) 不匹配给定选择器的选定元素。
.has(selector) 具有与 selector 匹配的后代元素的选定元素。

后代

遍历方法 返回一个包含...的 jQuery 对象
.find(selector) 与选择器匹配的后代元素。
.contents() 子节点(包括文本节点)。
.children([selector]) 子节点,可选择由选择器进行过滤。

兄弟

遍历方法 返回一个包含...的 jQuery 对象
.next([selector]) 每个选定元素后面紧邻的兄弟元素,可选择由选择器进行过滤。
.nextAll([selector]) 每个选定元素后面的所有兄弟元素,可选择由选择器进行过滤。
.nextUntil([selector], [filter]) 每个选定元素后面的所有兄弟元素,直到但不包括第一个匹配 selector 的元素,可选择由附加选择器进行过滤。
.prev([selector]) 每个选定元素前面紧邻的兄弟元素,可选择由选择器进行过滤。
.prevAll([selector]) 每个选定元素之前的所有兄弟元素,可选择由选择器进行过滤。
.prevUntil([selector], [filter]) 每个选定元素前面的所有兄弟元素,直到但不包括第一个匹配 selector 的元素,可选择由附加选择器进行过滤。
.siblings([selector]) 所有兄弟元素,可选择由选择器进行过滤。

祖先

遍历方法 返回一个包含...的 jQuery 对象
.parent([selector]) 每个选定元素的父元素,可选择由选择器进行过滤。
.parents([selector]) 所有祖先元素,可选择由选择器进行过滤。
.parentsUntil([selector], [filter]) 每个选定元素的所有祖先元素,直到但不包括第一个匹配 selector 的元素,可选择由附加选择器进行过滤。
.closest(selector) 从选定元素开始,并在 DOM 树中沿着其祖先移动,找到与选择器匹配的第一个元素。
.offsetParent() 第一个选定元素的定位父元素,可以是相对定位或绝对定位。

集合操作

遍历方法 返回一个包含...的 jQuery 对象
.add(selector) 选定的元素,加上与给定选择器匹配的任何其他元素。
.addBack() 选定的元素,加上内部 jQuery 堆栈上先前选择的一组元素。
.end() 内部 jQuery 堆栈上先前选择的一组元素。
.map(callback) 在每个选定元素上调用回调函数的结果。
.pushStack(elements) 指定的元素。

处理选定元素

穿越方法 描述
.is(selector) 确定任何匹配元素是否被给定的选择器表达式匹配。
.index() 获取匹配元素相对于其兄弟元素的索引。
.index(element) 获取给定 DOM 节点在匹配元素集合中的索引。
$.contains(a, b) 确定 DOM 节点b是否包含 DOM 节点a
.each(callback) 遍历匹配的元素,为每个元素执行callback
.length 获取匹配元素的数量。
.get() 获取与匹配元素对应的 DOM 节点数组。
.get(index) 获取给定索引处匹配元素对应的 DOM 节点。
.toArray() 获取与匹配元素对应的 DOM 节点数组。

事件方法

为了对用户行为做出反应,我们需要使用这些事件方法注册我们的处理程序。请注意,许多 DOM 事件仅适用于特定的元素类型;这些细微之处在此未涉及。事件方法在第三章中详细讨论,处理事件

绑定

事件方法 描述
.ready(handler) 绑定handler以在 DOM 和 CSS 完全加载时调用。
.on(type, [selector], [data], handler) 绑定handler以在给定类型的事件发送到元素时调用。如果提供了selector,则执行事件委托。
.on(events, [selector], [data]) 根据events对象参数中指定的多个事件为事件绑定多个处理程序。
.off(type, [selector], [handler]) 删除元素上的绑定。
.one(type, [data], handler) 绑定handler以在给定类型的事件发送到元素时调用。在处理程序被调用时删除绑定。

缩略绑定

事件方法 描述
.blur(handler) 绑定handler以在元素失去键盘焦点时调用。
.change(handler) 绑定handler以在元素的值更改时调用。
.click(handler) 绑定handler以在单击元素时调用。
.dblclick(handler) 绑定handler以在元素被双击时调用。
.focus(handler) 绑定handler以在元素获得键盘焦点时调用。
.focusin(handler) 绑定handler以在元素或后代获得键盘焦点时调用。
.focusout(handler) 绑定handler以在元素或后代失去键盘焦点时调用。
.keydown(handler) 绑定handler以在按键按下且元素具有键盘焦点时调用。
.keypress(handler) 绑定handler以在发生按键事件且元素具有键盘焦点时调用。
.keyup(handler) 当释放按键且元素具有键盘焦点时调用handler
.mousedown(handler) 当鼠标按钮在元素内按下时调用handler
.mouseenter(handler) 当鼠标指针进入元素时调用handler。不受事件冒泡影响。
.mouseleave(handler) 当鼠标指针离开元素时调用handler。不受事件冒泡影响。
.mousemove(handler) 当鼠标指针在元素内移动时调用handler
.mouseout(handler) 当鼠标指针离开元素时调用handler
.mouseover(handler) 当鼠标指针进入元素时调用handler
.mouseup(handler) 当鼠标按钮在元素内释放时调用handler
.resize(handler) 当元素大小改变时调用handler
.scroll(handler) 当元素的滚动位置发生变化时调用handler
.select(handler) 当元素中的文本被选择时绑定handler
.submit(handler) 当表单元素提交时调用handler
.hover(enter, leave) 当鼠标进入元素时绑定enter,当鼠标离开时绑定leave

触发

事件方法 描述
.trigger(type, [data]) 在元素上触发事件的处理程序,并执行事件的默认操作。
.triggerHandler(type, [data]) 在元素上触发事件的处理程序,而不执行任何默认操作。

简写触发

事件方法 描述
.blur() 触发blur事件。
.change() 触发change事件。
.click() 触发click事件。
.dblclick() 触发dblclick事件。
.error() 触发error事件。
.focus() 触发focus事件。
.keydown() 触发keydown事件。
.keypress() 触发keypress事件。
.keyup() 触发keyup事件。
.select() 触发select事件。
.submit() 触发submit事件。

实用程序

事件方法 描述
$.proxy(fn, context) 创建一个以给定上下文执行的新函数。

效果方法

这些效果方法可用于对 DOM 元素执行动画。有关详细信息,请参阅第四章 样式和动画

预定义效果

效果方法 描述
.show() 显示匹配的元素。
.hide() 隐藏匹配的元素。
.show(speed, [callback]) 通过动画heightwidthopacity显示匹配的元素。
.hide(speed, [callback]) 通过动画heightwidthopacity隐藏匹配元素。
.slideDown([speed], [callback]) 通过滑动动作显示匹配元素。
.slideUp([speed], [callback]) 通过滑动动作隐藏匹配元素。
.slideToggle([speed], [callback]) 显示或隐藏匹配元素并带有滑动动作。
.fadeIn([speed], [callback]) 通过使元素淡入到不透明来显示匹配元素。
.fadeOut([speed], [callback]) 通过使元素淡出到透明来隐藏匹配元素。
.fadeToggle([speed], [callback]) 显示或隐藏匹配元素并带有幻灯片动画。
.fadeTo(speed, opacity, [callback]) 调整匹配元素的不透明度。

自定义动画

效果方法 描述
.animate(properties, [speed], [easing], [callback]) 执行指定 CSS 属性的自定义动画。
.animate(properties, options) 一个更低层次的.animate()接口,允许控制动画队列。

队列操作

效果方法 描述
.queue([queueName]) 检索第一个匹配元素上的函数队列。
.queue([queueName], callback) callback添加到队列的末尾。
.queue([queueName], newQueue) 用新队列替换当前队列。
.dequeue([queueName]) 在队列上执行下一个函数。
.clearQueue([queueName]) 清空所有待处理函数的队列。
.stop([clearQueue], [jumpToEnd]) 停止当前运行的动画,然后启动排队的动画(如果有的话)。
.finish([queueName]) 停止当前运行的动画,立即将所有排队的动画推进到它们的目标值。
.delay(duration, [queueName]) 在执行队列中的下一项之前等待duration毫秒。
.promise([queueName], [target]) 返回一个 Promise 对象,一旦集合上的所有排队动作完成,就会被解析。

DOM 操作方法

DOM 操作方法在第五章中详细讨论,操作 DOM

属性和属性

操作方法 描述
.attr(key) 获取名为key的属性。
.attr(key, value) 将名为key的属性设置为value
.attr(key, fn) 将名为key的属性设置为fn的结果(分别在每个匹配元素上调用)。
.attr(obj) 设置以键值对形式给出的属性值。
.removeAttr(key) 删除名为key的属性。
.prop(key) 获取名为key的属性。
.prop(key, value) 设置名为key的属性为value
.prop(key, fn) 将名为key的属性设置为fn的结果(分别在每个匹配元素上调用)。
.prop(obj) 设置以键值对形式给出的属性值。
.removeProp(key) 移除名为key的属性。
.addClass(class) 将给定的类添加到每个匹配元素中。
.removeClass(class) 从每个匹配元素中删除给定的类。
.toggleClass(class) 如果存在,则从每个匹配元素中删除给定的类,并在不存在时添加。
.hasClass(class) 如果任何匹配的元素具有给定的类,则返回true
.val() 获取第一个匹配元素的值属性。
.val(value) 将每个元素的值属性设置为value

内容

操作方法 描述
.html() 获取第一个匹配元素的 HTML 内容。
.html(value) 将每个匹配元素的 HTML 内容设置为 value。
.text() 将所有匹配元素的文本内容作为单个字符串获取。
.text(value) 将每个匹配元素的文本内容设置为value

CSS

操作方法 描述
.css(key) 获取名为key的 CSS 属性。
.css(key, value) 将名为key的 CSS 属性设置为value
.css(obj) 设置以键值对给出的 CSS 属性值。

尺寸

操作方法 描述
.offset() 获取第一个匹配元素相对于视口的顶部和左侧像素坐标。
.position() 获取第一个匹配元素相对于.offsetParent()返回的元素的顶部和左侧像素坐标。
.scrollTop() 获取第一个匹配元素的垂直滚动位置。
.scrollTop(value) 将所有匹配元素的垂直滚动位置设置为value
.scrollLeft() 获取第一个匹配元素的水平滚动位置。
.scrollLeft(value) 将所有匹配元素的水平滚动位置设置为value
.height() 获取第一个匹配元素的高度。
.height(value) 将所有匹配元素的高度设置为value
.width() 获取第一个匹配元素的宽度。
.width(value) 将所有匹配元素的宽度设置为value
.innerHeight() 获取第一个匹配元素的高度,包括填充,但不包括边框。
.innerWidth() 获取第一个匹配元素的宽度,包括填充,但不包括边框。
.outerHeight(includeMargin) 获取第一个匹配元素的高度,包括填充,边框和可选的外边距。
.outerWidth(includeMargin) 获取第一个匹配元素的宽度,包括填充,边框和可选的外边距。

插入

操作方法 描述
.append(content) content插入到每个匹配元素的内部末尾。
.appendTo(selector) 将匹配的元素插入到由selector匹配的元素内部的末尾。
.prepend(content) content插入到每个匹配元素的内部开头。
.prependTo(selector) 将匹配的元素插入到由selector匹配的元素的内部开头。
.after(content) 在每个匹配的元素之后插入content
.insertAfter(selector) 在每个由selector匹配的元素之后插入匹配的元素。
.before(content) 在每个匹配的元素之前插入content
.insertBefore(selector) 将匹配的元素插入到由selector匹配的每个元素之前。
.wrap(content) 将每个匹配的元素包裹在content中。
.wrapAll(content) 将所有匹配的元素作为单个单元包裹在content中。
.wrapInner(content) 将每个匹配元素的内部内容包裹在content中。

替换

操作方法 描述
.replaceWith(content) content替换匹配的元素。
.replaceAll(selector) 用匹配的元素替换由selector匹配的元素。

移除

操作方法 描述
.empty() 移除每个匹配元素的子节点。
.remove([selector]) 从 DOM 中删除匹配的节点(可选地通过selector过滤)。
.detach([selector]) 从 DOM 中删除匹配的节点(可选地通过selector过滤),保留附加到它们的 jQuery 数据。
.unwrap() 删除元素的父元素。

复制

操作方法 描述
.clone([withHandlers], [deepWithHandlers]) 复制所有匹配的元素,可选择地也复制事件处理程序。

数据

操作方法 描述
.data(key) 获取第一个匹配元素关联的名为key的数据项。
.data(key, value) 将名为key的数据项与每个匹配的元素关联到value
.removeData(key) 删除与每个匹配元素关联的名为key的数据项。

Ajax 方法

通过调用其中一个 Ajax 方法,我们可以在不需要页面刷新的情况下从服务器检索信息。Ajax 方法在第六章中详细讨论,使用 Ajax 发送数据

发出请求

Ajax 方法 描述
$.ajax([url], options) 使用提供的选项集进行 Ajax 请求。这是一个低级方法,通常通过其他便利方法调用。
.load(url, [data], [callback]) 发送 Ajax 请求到url,并将响应放入匹配的元素中。
$.get(url, [data], [callback], [returnType]) 使用GET方法向url发出 Ajax 请求。
$.getJSON(url, [data], [callback]) 发送 Ajax 请求到url,将响应解释为 JSON 数据结构。
$.getScript(url, [callback]) 发送 Ajax 请求到url,执行响应作为 JavaScript。
$.post(url, [data], [callback], [returnType]) 使用POST方法向url发出 Ajax 请求。

请求监控

Ajax 方法 描述
.ajaxComplete(handler) handler绑定到在任何 Ajax 事务完成时调用。
.ajaxError(handler) handler绑定到任何 Ajax 事务以错误完成时调用。
.ajaxSend(handler) handler绑定到在任何 Ajax 事务开始时调用。
.ajaxStart(handler) handler绑定到在任何 Ajax 事务开始且没有其他事务活动时调用。
.ajaxStop(handler) handler绑定到在任何 Ajax 事务结束且没有其他事务仍在进行时调用。
.ajaxSuccess(handler) handler绑定到在任何 Ajax 事务成功完成时调用。

配置

Ajax 方法 描述
$.ajaxSetup(options) 为所有后续 Ajax 事务设置默认选项。
$.ajaxPrefilter([dataTypes], handler) | 在$.ajax()处理之前修改每个 Ajax 请求的选项。
$.ajaxTransport(transportFunction) 定义 Ajax 事务的新传输机制。

实用工具

Ajax 方法 描述
.serialize() 将一组表单控件的值编码为查询字符串。
.serializeArray() 将一组表单控件的值编码为 JavaScript 数据结构。
$.param(obj) 将键值对的任意对象编码为查询字符串。
$.globalEval(code) 在全局上下文中评估给定的 JavaScript 字符串。
$.parseJSON(json) 将给定的 JSON 字符串转换为 JavaScript 对象。
$.parseXML(xml) 将给定的 XML 字符串转换为 XML 文档。
$.parseHTML(html) 将给定的 HTML 字符串转换为一组 DOM 元素。

延迟对象

延迟对象及其承诺使我们能够以方便的语法对长时间运行的任务的完成做出反应。它们在第十一章,高级效果中详细讨论。

对象创建

函数 描述
$.Deferred([setupFunction]) 返回一个新的延迟对象。
$.when(deferreds) 返回一个承诺对象,以便在给定的延迟对象解决时解决。

Deferred 对象的方法

方法 描述
.resolve([args]) 将对象的状态设置为已解决。
.resolveWith(context, [args]) 将对象的状态设置为已解决,同时使关键字this在回调中指向context
.reject([args]) 将对象的状态设置为被拒绝。
.rejectWith(context, [args]) 将对象的状态设置为被拒绝,同时使关键字this在回调中指向context
.notify([args]) 执行任何进度回调。
.notifyWith(context, [args]) 在执行任何进度回调时,使关键字this指向context
.promise([target]) 返回与此延迟对象对应的 promise 对象。

promise 对象的方法

方法 描述
.done(callback) 在对象被解决时执行callback
.fail(callback) 在对象被拒绝时执行callback
.catch(callback) 在对象被拒绝时执行callback
.always(callback) 在对象被解决或被拒绝时执行callback
.then(doneCallbacks, failCallbacks) 当对象被解决时执行doneCallbacks,或当对象被拒绝时执行failCallbacks
.progress(callback) 每次对象接收到进度通知时执行callback
.state() 根据当前状态返回'pending''resolved''rejected'

杂项属性和函数

这些实用方法不能很好地归入之前的类别,但在使用 jQuery 编写脚本时通常非常有用。

jQuery 对象的属性

属性 描述
$.ready 一个 promise 实例,一旦 DOM 准备就绪就解决。

数组和对象

函数 描述
$.each(collection, callback) 遍历collection,为每个项执行callback
$.extend(target, addition, ...) 通过从其他提供的对象中添加属性修改对象target
$.grep(array, callback, [invert]) 使用callback作为测试过滤array
$.makeArray(object) object转换为数组。
$.map(array, callback) 构造由对每个项调用callback的结果组成的新数组。
$.inArray(value, array) 判断value是否在array中。
$.merge(array1, array2) 合并array1array2的内容。
$.unique(array) array中删除任何重复的 DOM 元素。

对象内省

函数 描述
$.isArray(object) 判断object是否为真正的 JavaScript 数组。
$.isEmptyObject(object) 判断object是否为空。
$.isFunction(object) 判断object是否为函数。
$.isPlainObject(object) 判断object是否以对象字面量形式创建或使用new Object创建。
$.isNumeric(object) 判断object是否为数值标量。
$.isWindow(object) 判断object是否表示浏览器窗口。
$.isXMLDoc(object) 判断object是否为 XML 节点。
$.type(object) 获取object的 JavaScript 类。

其他

函数 描述
$.trim(string) string的两端删除空格。
$.noConflict([removeAll]) | 将$恢复到其 jQuery 前定义。
$.noop() 一个什么也不做的函数。
$.now() 自纪元以来的当前时间(毫秒)。
$.holdReady(hold) 阻止ready事件的触发,或释放此保持。

标签:jQuery,jQuery3,元素,使用,手册,学习,选择器,方法,我们
From: https://www.cnblogs.com/apachecn/p/18200684

相关文章

  • 【PYTHON3】环境搭建+编程学习之路的开始——Windows系统
    一、概述在学习python开发语言之前需要安装好开发语言环境(也就是常说的开发环境)开发环境主要有:解释器和编辑器IDE,而其中的解释器是用来将代码转换成机器语言,python语言也就是解释器;编辑器用来写代码逻辑,python语言推荐的是pycharm,它是IDE集成开发环境,这里面有开发时需要的工具......
  • 【PB案例学习笔记】-02 目录浏览器
    写在前面这是PB案例学习笔记系列文章的第二篇,该系列文章适合具有一定PB基础的读者,通过一个个由浅入深的编程实战案例学习,提高编程技巧,以保证小伙伴们能应付公司的各种开发需求。文章中设计到的源码,小凡都上传到了gitee代码仓库https://gitee.com/xiezhr/pb-project-example.git......
  • rthread学习记录汇总-不断更新
    1、rthread同Linux类似,包含了所有主流的芯片、cpu架构,可从官方获取最新的rt-thread源码后进行裁剪 2、可从rthread官网下载env工具,env工具可用来对rtthread源码生成mdk/iar工程命令式scons--target=mdk5  scons--targe=iarscons自带的编译固件功能,命令为scnons,默认用......
  • salesforce零基础学习(一百三十七)零碎知识点小总结(九)
    本篇参考: https://help.salesforce.com/s/articleView?id=release-notes.rn_lab_conditional_visibiliy_tab.htm&release=250&type=5https://help.salesforce.com/s/articleView?id=release-notes.rn_automate_flow_builder_automation_lightning_app.htm&release=......
  • 数据结构学习笔记-判断是否为无向图
    判断是否为无向图问题描述:设图G用邻接矩阵A[n+1,n+1]表示,设计算法以判断G是否是无向图。【算法设计思想】遍历矩阵使用两层嵌套的for循环,外层循环变量......
  • 热更学习笔记10~11----lua调用C#中的List和Dictionary、拓展类中的方法
    [10]Lua脚本调用C#中的List和Dictionary调用还是在上文中使用的C#脚本中Student类:lua脚本:print("------------访问使用C#脚本中的List和Dictionary-----------")student.list:Add(2024)student.list:Add(5)student.list:Add(18)locallistSize=student.list.Countprin......
  • SimCLR: 一种视觉表征对比学习的简单框架《A Simple Framework for Contrastive Learn
    现在是2024年5月18日,好久没好好地看论文了,最近在学在写代码+各种乱七八糟的事情,感觉要和学术前沿脱轨了(虽然本身也没在轨道上,太菜了),今天把师兄推荐的一个框架的论文看看(视觉CV领域的)。20:31,正经的把这篇论文看完。论文:ASimpleFrameworkforContrastiveLearningofVisua......
  • 【Python】强化学习SARSA走迷宫
    之前有实现Q-Learning走迷宫,本篇实现SARSA走迷宫。Q-Learning是一种off-policy算法,当前步采取的决策action不直接作用于环境生成下一次state,而是选择最优的奖励来更新Q表。更新公式:SARSA是一种on-policy算法,当前步采取的策略action既直接作用于环境生成新的state,也用来更新Q表......
  • 关于学习VUE源码的感受! 学习VUE源码最好的方式 !!!
    仓库地址仓库whoelse666mini-vue崔学社mini-vue文章导航Vue3源码实战课|构建你自己的Vue3|掌握源码最有效的学习方法就是手写一遍!Vue3源码实战课阮一峰推荐最佳学习vue3源码的利器-mini-vue学习源码经历过程vue从出来到现在也有好些年了,相信几乎所所有从事......
  • cxCheckComboBox1学习(22)
    cxCheckComboBox1顾名思义,就是CheckBox与ComboBox的组合选择控件01]Item的添加 02]取已勾选的内容:cxCheckComboBox1.Text 03]取已勾选的内容:procedureTForm13.Button1Click(Sender:TObject);varidx,cnt:Integer;begincnt:=cxCheckComboBox1.Properties......