jQuery-入门指南-全-

jQuery 入门指南(全)

原文:Beginning jQuery

协议:CC BY-NC-SA 4.0

一、你需要知道的 JavaScript

jQuery 是一个建立在 JavaScript 之上的框架,它本身并不是一种语言。几乎没有任何 JavaScript 知识也可以编写 jQuery,但是我们不建议这样做。如果您希望能够自信地为您的站点编写 jQuery 插件,或者修改其他人编写的插件,您需要熟悉基本的 JavaScript。这就是为什么这本书从你需要知道的 JavaScript 开始。本章涵盖

  • 网页上的 JavaScript 脚本
  • JavaScript 中的变量和对象
  • JavaScript 函数
  • 条件式
  • 在数组和对象上循环
  • 调试 JavaScript

如果你熟悉 JavaScript,你可能想跳过这一章。这很好,但请考虑先略读一下,以确保你对所涉及的一切都感到满意。抵制跳到 jQuery 部分的诱惑——因为你会纠结于它。相信我们,在接下来的几章时间里,这些准备工作看起来都是值得的。我们在网上帮助过的许多开发人员都迫不及待地一头扎进了 jQuery,但很快就因为缺乏对 jQuery 语言的理解而停滞不前。当你写 jQuery 时,你写的是 JavaScript,但是使用的是 jQuery 库。在继续学习之前,确保你对本章的内容感到满意是非常重要的。我们建议您在阅读本章时尝试一下这些代码。不要自欺欺人地认为你理解了它,因为你已经读过了;没有什么可以代替你自己输入代码。

要运行代码,我们建议使用 JS 控制台( https://jsconsole.com ),这是 Remy Sharp 的一个工具,允许您执行 JavaScript 并查看结果。一些备选方案是 JS Bin ( http://jsbin.com )或 JSFiddle ( https://jsfiddle.net )。您可以在浏览器中输入代码并查看结果。这对短代码行非常有用。图 1-1 显示了 JS 控制台的示例。

img/A310335_2_En_1_Fig1_HTML.jpg)

图 1-1。

The Console tab displays the results of the JavaScript tab using JS bin

对于较大的代码,最好建立一个index.html页面,并在其中包含您的 JavaScript 文件。这将是你真正把一个网站放在一起的方式。下一节将解释如何做到这一点。在本章中,有几个例子使用了alert()函数来演示某个变量的值。这纯粹是用来演示概念的。在现实生活中,当您需要检查变量时,您不会使用警报—您会使用浏览器的 JavaScript 控制台。在本章的基本示例中使用警报的原因是它更容易上手。此时,不需要加载开发人员工具,这需要时间来适应。一旦你在本章的后面进入更复杂的代码,你将花时间探索开发者工具。

在网页上使用 JavaScript

在一个典型的 HTML 文件中,通常有两种方式向页面添加 JavaScript。要添加一些 JavaScript,您可以在一个script标签内内联添加代码,如下所示:

<script type="text/javascript">
  //write code here
</script>

或者,您可以创建一个带有.js文件扩展名的外部 JavaScript 文件,然后通过script标签加载它:

<script type="text/javascript" src="path/to/your/file.js"></script>

Note

你必须关闭script标签。

第一个位置在head元素中,第二个位置就在结束的</body>标签之前。过去,脚本总是被加载到head元素中,但是随着性能和页面加载速度比以往任何时候都更加重要,通常建议将脚本放在页面的底部。这也是我们支持的方法。

浏览器从上到下呈现页面,当它遇到您的脚本时,它暂停呈现页面以加载到您的 JS:。因此,页面加载较慢(或者,更重要的是,用户会有这种感觉),因为您加载的 JavaScript 文件阻止了呈现。因此,将脚本放在结束的</body>标签之前意味着当加载脚本的时候,页面的其余部分已经被加载了。

在开始研究语言本身之前,还有一点需要注意。使用 HTML5 doctype (<!DOCTYPE html>),您实际上不需要在脚本标签上定义 type 属性。简单地使用以下代码就足够了:

<script src="path/to/your/file.js"></script>

这在旧浏览器中不会引起问题 HTML5 doctype 也不会——我们强烈推荐使用它。

语法约定

JavaScript 的语法非常基本和清晰,但是在这个过程中您会发现一些微妙之处。通常有不止一种方法来做事情,但是社区有一些随着时间的推移而变得根深蒂固的惯例。

分号

我们想直接提到的一个约定是分号的使用。通常在 JavaScript 中,在行尾添加分号是可选的,你会看到教程没有这样做。然而,惯例是总是在一行的末尾使用分号,这就是我们在本书中要遵循的。显然在某些情况下你不能使用分号,你会在本书中看到,但是在任何分号可选的情况下,我们都会使用分号。我们建议您也这样做。

空格

另一个要考虑的是留白。在 JavaScript 中这是无关紧要的,所以你可以按照你喜欢的方式用空白来布局代码。无论何时你在一组大括号中,你都应该缩进一个制表符,但是除此之外,你会发现自己在适应自己的标准。

评论

在继续之前,在这个阶段值得讨论的意见。JavaScript 允许您在代码中插入注释。这些内容将被忽略,不会被视为代码,因此您可以在注释中添加任何内容。插入注释对于记录代码很有用。注释有两种语法,一种用于单行注释,另一种用于多行注释:

//this is a single-line comment, denoted by two forward slashes
/* this is a multiline comment, started with a slash and an asterisk
and ended with an asterisk and a slash */

使用注释来提醒自己一段代码及其作用,或者为未来的你提供参考。在很长一段时间没有从事代码工作之后,注释确实可以帮助你记住你为什么要写你所写的东西。

变量

通常在编码的时候,你想要保存一些东西的状态。也许你想记住你的背景的当前颜色是红色,或者你刚刚计算的总数是 33。和大多数语言一样,JavaScript 也有变量:存储信息的地方。要创建一个变量,只需用关键字var声明它,给它命名,然后将其设置为等于某个值。您也可以在不显式设置变量值的情况下声明变量。如果这样做,变量将被设置为undefined,这是 JavaScript 中的一个特殊值,仅仅意味着这个变量没有被设置为任何值。以下示例声明了三个变量:

var twoPlusThree = 5;
var twoPlusTwo = 2 + 2;
var notYetDefined;

第一个变量twoPlusThree被设置为值5。第二个,twoPlusTwo,被设置为2+2的结果。在这里你会遇到 JavaScript 的众多操作符之一,+。这些运算符对值执行运算。大部分都很明显。除了+(加法),还有(减法)、/(除法)、*(乘法),还有更多。你会在整本书中遇到更多,所以现在不要太担心他们。第三个变量notYetDefined没有值,设置为undefined,因为我们声明了一个变量(也就是我们创建了一个新变量)但是没有设置值。

创建变量

变量可以包含字母、数字和下划线。它们不能以数字开头。因此变量名0abc无效,而abc0有效。通常,大多数开发人员不在变量名中使用数字,而是坚持使用字母大小写或下划线符号。

Note

注意我们对变量的命名约定。我们使用的是所谓的 camelCase,这意味着变量名中的第一个单词应该以小写字母开头,但名称中的其他每个单词都应该以大写字母开头。我们将在整本书中使用这个约定。还有其他流行的命名约定,最著名的是 _ 下划线 _ 方法。这使所有单词保持小写,并用下划线分隔。这在其他语言中更流行。大多数 JavaScript 社区都使用 camelCase。

当然,一旦你设置了一个变量的值,并不意味着你不能改变这个值。所有变量的值都可以更改。这与声明变量的方式非常相似,唯一的区别是开头缺少了关键字var。只有在声明变量的时候才需要。这个例子将totalCost设置为 5,然后再次将其更新为 5 + 3(显然,您可以将它写成 8):

var totalCost = 5;
totalCost = 5 + 3;

类型

在继续之前,您会注意到,到目前为止,所有变量都被设置为非十进制数。在 JavaScript(和所有编程语言)中,有类型的概念。变量可以是几种类型中的任何一种。最常见的是数字类型和字符串类型。还有布尔型,只能设置为true或者false。使用 JavaScript 时,通常不必太担心类型。即使变量是用整数值(例如 5)声明的,也可以将其更新为字符串值,如下所示:

var testVariable = 5; testVariable = "Jack";

这就把testVariable的类型从整数变成了字符串,JavaScript 一点也不抱怨。除了字符串、数字和布尔值,您需要关注的另外两种类型(目前)是数组和对象。两者都将很快得到更详细的介绍,但是现在,只需要知道数组本质上是一个值的列表。这些值可以是任何类型,并且不是数组中的所有值都必须是同一类型。您可以通过在方括号之间列出值来创建数组,如下所示:

var squares = [1, 4, 9, 16, 25];

var mixed = [1, "Jack", 5, true, 6.5, "Franklin"];

现在,这就是你需要知道的关于数组的全部内容。

另一种类型,object,用一个例子更容易解释。假设您的应用中有一个汽车的概念。这辆车有一定数量的轮子和座位,有一定的颜色,有最大速度。你可以用四个独立的变量来模拟这辆车:

var carWheelCount = 4;
var carColor = "red";
var carSeatCount = 5;
var carMaximumSpeed = 99;

如果只有一个包含所有这些信息的变量——car——那就更容易了。这就是一个物体的作用。这是一种在一个变量中存储大量信息(通常是相关的)的方法。如果您使用对象,汽车的先前代码可能如下所示:

var car = {
  wheelCount: 4,
  color: "red",
  seatCount: 5,
  carMaximumSpeed: 99
};

创建对象的语法与您到目前为止看到的任何语法都有一点不同,所以让我们浏览一下。像平常一样创建变量,但是要创建一个对象,就要用花括号把它括起来。对象是一组键值对,也称为属性。通过以格式key: value列出它们来创建它们,在除最后一个属性之外的所有属性的末尾加上一个逗号。这是一种更好的以编程方式对代码建模的方式。

要访问对象中的属性,有两种选择:

car.wheelCount;
car["wheelCount"];

使用两种方法访问属性的原因很容易解释。大多数情况下,您将使用第一个版本,点符号。唯一需要使用第二个版本的时候是,如果你需要访问一个对象中的一个键,而这个键的名字存储在一个变量中。在演示中可以更清楚地看到这一点。假设您想要访问的键wheelCount,由于您的应用中的一些在先代码而存储在一个变量中。如果你想得到wheelCount的值,你必须使用第二种符号,如下所示:

var keyToGet = "wheelCount";
car[keyToGet]; //this will give us 4

这种情况不经常发生,但是有时候需要用到。在本书的后面,你会看到这样的例子。现在,让我们继续。

功能

一旦你写了一些你可能想在别处再次使用的代码,你有两个选择。当你需要使用它的时候,你可以简单的复制代码——但是这不是一个好的方法。如果你需要改变它,你必须在两个或更多的地方改变它。最好创建一个函数。

创建函数

这使您可以在多个地方重用代码,如果您需要进行更改,您只需在一个地方进行更改。创建一个函数非常简单。使用function关键字表示您正在创建一个新函数。然后命名它,并将函数代码放在花括号内。

function alertTwo() {
  alert("2");
}

这个功能只是在你的屏幕上显示一个显示“2”的警告。注意,函数名后面的括号是空的。这意味着你声明的函数没有任何参数。您可以声明另一个函数,该函数接受一个参数并发出警报,如下所示:

function alertSomething(something) {
  alert(something);
}

这个函数通过一个参数传递,这个参数在函数中是一个变量,您可以将其称为something。您所做的只是提醒该变量的值,如下所示:

alertSomething("Jack");
alertSomething(2);

如果在浏览器中运行这段代码,会弹出两个警告框,第一个显示文本“Jack”。一旦你点击警告框来消除它,另一个包含数字“2”的框将会弹出。

函数也可以接受多个参数,例如:

function alertThings(thing1, thing2) {
  alert(thing1);
  alert(thing2);
}

alertThings("Jack", "Franklin");

和前面的例子一样,这也给出了两个警告。第一个包含“杰克”,第二个包含“富兰克林”。

jQuery 开发中经常做的事情是将一个对象传递给一个函数,而不是多个变量。调用一个函数并传入多个参数会让人感到困惑;例如:

someFunction("Jack", "Franklin", 1, 2, 3, 4, "a", "x");

所以许多插件 jQuery 广泛使用的东西——将对象传递给函数。例如,如果您要声明一个带有三到四个或更多参数的函数,您可能会让该函数接受一个对象,如下所示:

function aPerson(person) {
  alert(person.firstName);
  alert(person.lastName);
  alert(person.age);
}

var jack = {
  firstName: "Jack",
  lastName: "Franklin",
  age: 20
}

aPerson(jack);

如果运行该代码,您将看到三个警告,每个警告都警告存储在jack变量中的对象的属性。这是在大量使用 jQuery 时使用的一种模式,所以一定要理解这里发生了什么。为了避免向函数传递大量参数——这使得很难记住哪个参数是哪个参数以及它们进入的顺序——开发人员通常编写他们的函数来接受一个对象作为唯一的参数。这意味着每个参数都可以被命名——顺序并不重要——作为开发人员,查看代码并了解发生了什么要容易得多。

与其现在讨论函数及其所有细节,不如在后面的章节中讨论它们。然而,在继续之前,您需要理解函数返回值的概念。

返回值的函数

函数通常用作执行某些计算的方法,例如将英寸转换为厘米。这是一个你期望传入一个值的函数,它计算并“返回”一个值。以下示例显示了如何实现这一点:

function inchesToCM(inches) {
  return inches * 2.54;
}

var sixFeetInInches = 72;
var sixFeetInCM = inchesToCM(sixFeetInInches);

这就剩下sixFeetInCM为 182.88,也就是 72 乘以 2.54。给sixFeetInCM变量赋予那个值的原因是因为inchesToCM()函数返回它的自变量——英寸——乘以 2.54。通过返回参数,sixFeetInCM变量被设置为inches * 2.54给你的值。

函数绝对可以返回任何值。通常,您可能希望返回一个布尔值truefalse,如下所示:

function isItSunnyInBritain() {
  return false;
}

var isSunny = isItSunnyInBritain();

这个函数将返回false,这是应该的。面对现实吧,英国从来都不是晴天!从函数返回值是你会经常用到的。

条件式

您经常想做的事情是有条件地运行代码。也就是说,只有在其他事情为真或为假的情况下才做某事。例如,如果age变量小于 12,则警告“孩子”。JavaScript 通过if语句拥有这种能力:

var age = 10;
if(age < 12) {
  alert("Child");
}

但是如果年龄大于 12 岁,你想做别的事情呢?除了if语句,您还可以在它的末尾附加一个else,如下所示:

var age = 15;
if(age < 12) {
  alert("Child");
} else {
  alert("Not a child");
}

这里您遇到了另一个操作符—小于符号,<。还有它的反义词,大于,>,还有“小于等于”和“大于等于”,<=>=。如果你想检查多个条件,你也可以使用else if,就像这样:

if(age <= 12) {
  alert("Child");
} else if (age < 20) {
  alert("Teenager");
} else {
  alert("Adult");
}

当然,如果需要,您可以使用多个else if语句,但是通常您不需要多于一个或者两个。任何可以评估为truefalse的东西都可以作为if语句的条件。一个更简单的方法是想象把一些陈述放在这些括号里,在你的头脑中计算这个陈述是对还是错。如果你能做到这一点,你的条件可以用在一个if语句中。

var name = "Jack";
var age = 20;

if(age > 18 && name === "Jack") {
  alert("Hello Jack, you’re older than 18!");
}

这里有两件新的事情要讨论。首先,您用 and 运算符将两个条件组合成一个,&&.这意味着只有当条件的左右两边都计算为true时,条件才会计算为true

其次,您已经看到了如何检查等式。在 JavaScript 中,这是一个复杂的领域。您可以同时使用=====来检查相等性,两者都有细微但重要的区别。现在,当我们告诉你总是使用===时,请相信我们。

除了&&,还有||,它是“或”操作符。让我们来看看实际情况:

var age = 19;
var name = "bob"; if(age > 18 || name === "Jack") {
  alert("your name is Jack or you’re older than 18");
}

即使只有一个条件语句为真,警报仍会显示在这里。年龄确实大于 18,这使得这个人的名字不是 Jack 无关紧要,因为只要满足其中一个条件,or 运算符就会返回true

确保你理解了||&&.之间的区别,如果两个条件中的任何一个计算为true,那么第一个计算为true;而如果两个条件都评估为true,则&&评估为true

也可以否定条件句,也就是说,如果相反的情况成立,它们就通过了,如下所示:

var age = 20;
if(!age < 18) {
  alert("Hello adult");
}

求反运算符!反转条件运算的结果。在这个例子中,age < 18false,但是前缀为条件反转falsetrue!

一般来说,你应该尽量避免像前面那样的否定,把它写成age >= 18而不是!age < 18,因为这样代码更容易阅读。扫描代码和评估其功能越快越好。

使用控制台调试

前面,我们简要地提到了浏览器中可用的开发人员控制台。我们说过,一旦我们遇到更复杂的例子,我们将从使用alert()切换到使用console.log()。在进行这种转换之前,您需要查看一下您可以使用的调试。

现代浏览器附带了一个 JavaScript 控制台,这是 JavaScript 开发人员宝库中的一个无价工具。下表描述了如何在所有现代浏览器中访问控制台:

  • IE10+:按 F12 并单击控制台选项卡。
  • chrome:MAC OS 上的 Alt+Cmd+J。Windows 上的 Ctrl+Shift+J。
  • safari:MAC OS 上的 Alt+Cmd+I。Windows 上的 Ctrl+Alt+I。
  • 火狐:macOS 上的 Alt+Cmd+K。Windows 上的 Ctrl+Shift+K。
  • opera:MAC OS 上的 Alt+Cmd+I。Windows 上的 Ctrl+Shift+I。

我使用谷歌 Chrome 作为我的首选浏览器,本书中的所有截图都来自 Chrome 的控制台(除非另有说明),但所有浏览器都有非常相似的功能集,它们看起来都一样,所以选择一个最适合你的。请看图 1-2 中的例子。

img/A310335_2_En_1_Fig2_HTML.jpg)

图 1-2。

After declaring a variable, viewing its value in Google Chrome’s JS console

控制台非常适合尝试代码片段,但它更适合调试。最流行的方法是console.log(),它会将数据记录到控制台供您查看。从这一章开始,示例使用这种方法,而不是alert()。当处理复杂的数据结构时,console.log()提供了一种更好的查看变量值的方式。

要查看示例,请创建以下 HTML 文件(将其命名为 sensible ),然后使用开发人员工具在浏览器中打开它:

<!DOCTYPE html>
<html>
  <head>
    <title>Hey</title>
    <script type="text/javascript" charset="utf-8">
      console.log("Jack");
    </script>
  </head>
  <body>
  </body>
</html>

如果您按照前面的说明打开开发人员控制台,您应该会看到类似图 1-3 的内容。

img/A310335_2_En_1_Fig3_HTML.jpg)

图 1-3。

The string “Jack” being logged to the console

你可以把任何事情记录到控制台,它会知道如何处理。当您深入研究数组时,您将会看到这一点。

数组

在继续学习 jQuery 之前,了解数组是很重要的。如前所述,数组只是一个值的列表。下面是一个数组的示例:

var classMates = ["Jack", "Jamie", "Rich", "Will"];

这差不多就是前面所介绍的内容,所以现在是时候深入研究了。

您可以通过在变量后的方括号中添加一个数字来访问数组中的单个元素,如下所示:

classMates[1]; //Jamie

注意这里位置 1 的元素不是“Jack”,而是“Jamie”。这是因为数组是零索引的。也就是说,数组中的第一个元素实际上位于位置 0,而不是位置 1。如果你不是程序员,这可能需要一些时间来适应,但是一旦你掌握了它,它就会成为你的第二天性。因此,要从数组中获取名称“Jack”,您需要使用classMates[0]。您可以使用classMates.length找出数组的长度,在本例中返回 4。作为一个快速的测试,当你不知道数组的长度时,你认为如何得到数组的最后一个元素呢?

你应该这样做:

classMates[classMates.length - 1]; // "Will"

看看你是否能在不先阅读解释的情况下弄清楚这是如何工作的。classMates.length给出数组长度,即 4。因此,要获取数组中的最后一项,需要获取最后一个索引处的 person,即长度减一,直到第一个元素位于位置 0 而不是位置 1。

记住,JavaScript 数组中可以包含任何东西,包括对象和其他数组。这就是你所谓的二维数组,数组中的每个元素本身就是一个数组:

var twoDArray = [
  ["Jack", "Jon", "Fred"],
  ["Sue", "Heather", "Amy"]
];

要访问数组的数组中的元素,使用方括号符号,就像您之前使用的那样,来获取classMates数组中的第二个元素,classMates[1]:

twoDArray[0][0]; //Jack
twoDArray[1][0]; //Sue
twoDArray[1][2]; //Amy

第一组方括号抓取了twoDArray的元素,所以twoDArray[0]返回包含"Jack", "Jon","Fred"的数组。twoDArray[1]是包含"Sue", "Heather","Amy"的数组。

这并不是你必须经常做的事情,但是在这个 JavaScript 介绍中向你展示是值得的,因为它确实让你理解了数组的基础。

要向数组中添加元素,使用push()方法:

classMates.push("Catherine");

注意push()总是会在数组的末尾添加一个元素。

不幸的是,没有这样的方法可以轻松地删除数组中的项目。您可以使用delete操作符,乍一看,它可以完成您需要的一切:

delete classMates[1]

虽然这看起来可行,但实际上并不可行。如果您对初始数组“Jack”、“Jamie”、“Rich”、“Will”执行该命令,将会发生以下情况:

delete classMates[1];
console.log(classMates); //["Jack", undefined, "Rich", "Will"]

这是delete的关键之处:它不从数组中移除元素。它只是用undefined替换该索引处的值。因此,要真正从数组中完全删除一个元素,还需要做更多的工作。当这个问题实际发生时,您将在本书的后面部分重新讨论这个问题。

现在,您已经了解了使用数组的基本知识,可以开始学习循环了。很自然的,一旦你有了一个条目列表,你经常想要依次检查每一个条目,并对其执行一些计算或功能。您将在这里遇到的两个循环是while循环和for循环。

while循环非常简单,实际上采用了您已经看到的形式,即if语句。基本的while循环如下所示:

while(condition) {
        //code
}

当条件评估为true时,大括号内的代码将继续执行。这有许多用例,但最常见的是用于遍历一个列表,就像这样:

var count = 0;
while(count < classMates.length) {
  alert(classMates[count]);
  count++;
}

如果您要运行该代码,您将得到五个警报—“Jack”、“Jamie”等等,分别对应于classMates数组中的五个项目(在前面的示例中,您使用了push()方法来添加第五个项目,“Catherine”)。一行一行地看,它是这样工作的:

  • 首先,将一个新的count变量设置为 0。
  • 代码执行的条件是count变量必须小于classMates.length的长度。
  • 如果是,你要做两件事:
    • 首先警告classMates[count]处的值,它将是classMates[0],然后是classMates[1],直到classMates[3]——最后一次count变量小于classMates的长度。
    • 第二,运行count++,这是一个你没见过的新操作符。这只是count = count + 1的一个快捷方式,所以它将count变量加 1。

你会发现自己经常使用while循环。当然,它不一定要和数组一起使用——你可以在没有数组的情况下使用它,但是它会导致一个无限循环,所以我们不建议运行它。这里有一个例子:

while(1 < 5) {
  alert("hello");
}

在这里,条件 1 < 5 将始终为真,因此循环内的代码将被反复执行。大多数现代浏览器会检测到这一点,并防止代码使浏览器崩溃,但即使如此,我们也不建议运行它。

除了while循环,还有一个for循环。其语法略有不同:

for(before loop; condition; iteration) {
        //code
}

在一个for循环的参数中,您定义了三件事:

  • 循环开始前要运行的代码
  • 大括号内的代码可以执行所必须满足的条件
  • 每次迭代结束时运行的代码

最好用一个例子来说明这一点。下面将显示数字 0 到 9:

for(var i = 0; i < 10; i++) {
  alert(i);
}

如果您想使用一个for循环而不是一个while来循环通过classMates数组,可以这样做:

for(var i = 0; i < classMates.length; i++) {
  alert(classMates[i]);
}

将此与while循环进行比较:

var count = 0;
while(count < classMates.length) {
  alert(classMates[count]);
  count++;
}

唯一的区别是首字母var count = 0;被移到了for的括号内,而count++被移到了括号的末尾。开发人员通常会使用count作为变量来循环一些代码;其他时候,你会看到在“迭代器”中使用了i当然,您可以使用任何您喜欢的变量名,但这两个往往是最受欢迎的。我们将在本书的大部分内容中使用i,但是如果你喜欢更冗长的count,或者任何类似的东西,请随意使用。

使用while循环或for循环,可以在循环时编辑数组的值,如下所示:

var i = 0;
while(i < classMates.length) {
  classMates [i] = "Class Mate " + i;
  i++;
}

这会将您的classMates数组更新为

["Class Mate 0", "Class Mate 1", "Class Mate 2", "Class Mate 3"]

在本章结束之前,还有一件关于for循环的事情你需要知道。当处理一个对象时,你可以结合使用for循环和in操作符来循环属性:

var classMates = {
  "Jamie" : 20,
  "Will": 21,
  "Rich": 22,
  "Jack": 23
}

for(classMate in classMates) {
  console.log(classMate + " is " + classMates[classMate] + " years old");
}

这将为您提供以下输出:

Jamie is 20 years old

Will is 21 years old

Rich is 22 years old

Jack is 23 years old

这里的关键是第一行,for(classMate in classMates) {}。这将遍历classMates对象,并遍历对象中的每个属性。然后你可以通过classMates[classMate]得到那个属性的值。

More console.log()

您在查看数组时使用了console.log(),但是到目前为止,您只在最基本的形式中使用了它,通过向它传递一个参数,您希望它记录到控制台。比那强大多了。您可以传入多个参数,它会在同一行记录所有参数。例如:

var classMates = ["Jack", "Jamie", "Rich", "Will"];
var twoPlusTwo = 4;
console.log(classMates);
console.log("twoPlusTwo", twoPlusTwo);

您将看到如图 1-4 所示的输出。

img/A310335_2_En_1_Fig4_HTML.jpg)

图 1-4。

The console logging out your array and variable

您可以看到,注销classMates数组可以完全清楚它包含的内容,这正是console.log()存在的目的。如果希望在一行中输出多种内容,可以通过向函数传递多个参数来轻松实现。第二个例子注销字符串"twoPlusTwo",然后注销变量twoPlusTwo。我们经常在记录大量值时这样做,所以在控制台中哪一行记录了什么就更清楚了。我们将在本书中大量使用console.log()

摘要

本章涵盖了很多 JavaScript 基础知识,包括变量、if语句、循环、数组、对象等等,现在您已经有了坚实的基础。当您进入 jQuery 时,我们会定期停下来,以确保您对所做工作背后的 JavaScript 感到满意。系好安全带,因为在下一章中,是时候继续前进,认识 jQuery 了。

二、jQuery 的基础知识

jQuery 是一个强大而复杂的库,于 2006 年 8 月首次发布,尽管最初的想法出现得更早。在开始之前,我们有时间上一堂简短的历史课,介绍图书馆是如何产生的。

第一次在网上发布任何暗示潜在图书馆正在形成的消息是在 2005 年 8 月 22 日。jQuery 的创始人约翰·瑞西格(John Resig)发布了一篇名为“JavaScript 中的选择器”( https://johnresig.com/blog/selectors-in-javascript/ )的博文,展示了瑞西格的想法,即我们可以使用 CSS 选择器与 JavaScript 中的元素进行交互。这展示了一个新的想法,最终形成了我们今天所知道和喜爱的图书馆的雏形。jQuery 于 2006 年 1 月在纽约的 Bar Camp 正式发布,并迅速风靡互联网,占据了许多热门网站的首页。jQuery 不断发展,并在 2006 年 8 月达到稳定的 v1。从那以后,它继续增长。它对 web 开发的影响不可低估,对 JavaScript 的社区观点的影响更为重要。

在本章中,您将执行以下操作:

  • 看看浏览器如何通过文档对象模型(DOM)表示网页。
  • 看看 web 页面上下文中的 DOM 节点和术语父节点、子节点和兄弟节点。
  • 下载 jQuery 源代码并将其包含在网页中。
  • 编写一些利用 jQuery 的代码。
  • 详细探究代码是如何工作的,并了解 jQuery 的一些特性。
  • 探索 jQuery API 文档以及如何使用它来回答您可能遇到的任何问题。

jQuery 让“普通”开发人员更容易理解 JavaScript。例如,在通过 ID 选择元素时,您更喜欢以下两种语法中的哪一种?

document.getElementById("example");

或者

$("#example");

突然,如果您知道如何用 CSS 选择元素,您可以通过使用 jQuery 将这些知识转移到 JavaScript。jQuery 提供了一种与文档对象模型交互的可靠的跨浏览器方法。在我们继续深入之前,是时候讨论 DOM 了。

文档对象模型(DOM)

当你浏览一个网站时,你会看到许多元素组合在一起,形成你面前的东西。为了能够通过代码访问这些元素来删除、添加和操作它们,您需要某种形式的接口——页面上元素的表示,它是结构化的,并遵循一组关于如何对它们建模的规则。这就是大教堂。DOM 还允许您捕获浏览器事件——比如用户点击链接、提交表单或向下滚动页面。在第三章中,你将看到如何使用 jQuery 来遍历 DOM。

在网络和浏览器的早期,JavaScript 实现的标准并不十分清晰。这导致浏览器以不同的方式实现功能,这给开发人员带来了问题。这导致任何 JavaScript 都必须为具有不同实现的不同浏览器编写多次,主要是 Netscape 和 Internet Explorer (IE)。

幸运的是,随着事情的进展,浏览器采用了相同的标准,事情也解决了。然而,浏览器支持 DOM 的水平在今天仍然会引起问题。特别是,我们不能摆脱旧版本的 Internet Explorer,它不支持 DOM 到更现代的浏览器的水平。这是 jQuery 如此有价值的一个原因:它提供的一切在旧版本的 IE 中都能很好地工作,就像在最新版本的 Google Chrome 或 Mozilla Firefox 中一样。需要注意的是,IE 的最新版本是 11;微软 Edge 现在是 Windows 10 的默认浏览器。

在继续使用 jQuery 之前(您很快就会明白了!),有必要花点时间介绍一下 DOM 是如何工作的。当一个页面被加载时,浏览器生成一个页面内容的表示,对于每个元素,它生成一个或多个表示它的节点。节点有多种类型,如果这是一本纯粹关于 DOM 与 JavaScript 交互的书,我们将会更详细地讨论 DOM。

正如我们在第一章中提到的,我们认为给刚接触 jQuery 的人一个坚实的 jQuery 基础介绍是非常重要的。我们已经非常详细地介绍了 JavaScript,我们觉得看一下 DOM 很重要。当浏览器将当前页面表示为 DOM 时,每个元素都是一个节点。假设您有一个包含一些文本的段落,例如:

<p>Hello World</p>

那不是一个节点,而是两个节点。有一个包含“Hello World”的文本节点和一个段落的元素节点。

Note

文本节点将是元素节点的子节点,因为它驻留在元素节点中。在一个典型的页面中,有许多嵌套的节点。

包含两个段落且两个段落中都有文本的,div的结构如下:

div element node
-- paragraph element node
---- text node
-- paragraph element node
---- text node

这个实例中的两个段落是兄弟,因为它们有相同的父节点。段落是div的子节点,但是文本节点不是子节点,因为它们不是div元素的直接后代。它们是段落节点的子节点。您需要了解三种主要的节点类型:元素、文本和属性节点。假设你给了段落一个类,比如:

<p class="intro">Hello World</p>

现在有三个节点在起作用:

  • 代表段落的元素节点
  • 包含文本“Hello World”的文本节点
  • 表示该元素的属性节点有class="intro"

有点令人困惑的是,属性节点不被认为是元素节点的子节点。

在这些元素之间,它们构成了绝大多数网页的绝大部分。在(最后)开始学习 jQuery 之前,请确保您理解了以下术语,因为它们在本书中会不断出现:

  • 子节点:是另一个节点的直接后代的节点,通常是元素节点
  • 父节点:具有直接后代的节点(例如,子节点)
  • 兄弟节点:共享同一父节点的两个节点

最后再重复一次,下面是一个可视化的表示:

div parent

-- p child of div, sibling of p

---- "hello world" - child of p

-- p child of div, sibling of p

---- strong child of p

------ "hello" child of strong

当我们在本书后面讨论用 jQuery 选择元素时,理解术语“子元素”、“父元素”和“兄弟元素”将非常重要,所以请确保您对它们的含义有信心。

正在下载 jQuery

在做了大量准备之后,您已经准备好开始第一次使用 jQuery 了。最好的起点是位于 http://jquery.com 的 jQuery 网站(见图 2-1 )。

img/A310335_2_En_2_Fig1_HTML.jpg)

图 2-1。

The jQuery home page

单击 jQuery 主页上的大下载 jQuery 按钮(或下载选项卡)打开下载页面。这里给出了将 jQuery 添加到项目中的许多方法。例如,如果您想在本地添加 jQuery,您可以下载压缩版本或未压缩版本。如果您正在使用 npm(节点包管理器)或 Bower(另一个包管理器),您可以在这里找到说明。此外,还有使用 CDN(内容交付网络)和其他一些方式的说明。

如果您单击主页上的“下载 jQuery”按钮。下载 jQuery 有多种选择。下载页面上列出了两个压缩级别选项:

  • 生产(87KB),缩小并压缩
  • 开发(268KB),未压缩代码

除非您想详细研究使用 jQuery 创建的每个项目的 jQuery 源代码,否则请始终选择产品版本。这段代码是通过一个 minifier 运行的,mini fier 是一个将 JavaScript 文件压缩成较小版本的程序。缩小器执行许多操作来使代码尽可能小,包括

  • 去掉所有的空白。
  • 删除所有注释。
  • 重命名长变量名;例如,var myCar可能会变成var a

精简的代码是完全不可读的,但它并不是为了可读而设计的——它的设计是为了使文件尽可能小。从现在开始,当我们提到 jQuery 源代码时,我们指的是 jQuery 的缩小版。

一些开发者链接到一个 CDN 托管版本的 jQuery,其中最流行的是谷歌的 CDN ( https://developers.google.com/speed/libraries/#jquery )。这些允许您通过引用驻留在 CDN 上的 jQuery 文件来包含 jQuery。如果您想包含来自 Google CDN 的最新版本的 jQuery,您可以这样做:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>

这样做有好处。如果用户访问了以这种方式引用 jQuery 的另一个站点,他们可能已经缓存了该文件,这意味着浏览器不必再次下载它。然而,对于本书中的示例,我们选择在本地下载一个版本的 jQuery,原因很简单:您不需要通过互联网来完成这些示例。这听起来可能很愚蠢,但不止一次,我们的一位作者在火车上准备做一些工作,只记得他参考了 Google CDN 版本的 jQuery,而且他没有互联网连接。

jQuery API 文档

如果您正在使用 jQuery,您需要一个很好的来源来了解每个 API 的功能。jQuery 文档( http://api.jquery.com )列出了 jQuery 提供的每一种方法。jQuery 如此成功的另一个原因是它的文档,这很棒。我们不能夸大我们认为文档有多好(见图 2-2 )。

img/A310335_2_En_2_Fig2_HTML.jpg)

图 2-2。

The jQuery API index page

有几种方法可以在网站上找到你想要的东西。如果你确切地知道你想要哪种方法,使用位于屏幕右上角的搜索框是最快的方法。如果您不确定您到底想要什么——也许您正在寻找一种方法来做一些特定的事情,但是您不确定它是否存在——您可以浏览屏幕左侧列出的 jQuery API 类别来缩小搜索范围。你现在还不需要看这些,但是你会多次回到 API。把它放在你的书签栏上,或者找一个简单的方法浏览它,因为你会经常用到它。

编写一些 jQuery

将下载的 jQuery 文件作为jquery.js保存在机器上的新文件夹中。您还将向该文件夹添加一个 HTML 文件,因此也创建一个index.html页面。最后,您想在一个单独的文件中编写所有的 JavaScript,所以创建app.js。此代码在02/code/ex1内可用。

在您选择的编辑器中加载 HTML 页面——我个人使用 Vim 我们强烈推荐 Sublime Text 2 ( www.sublimetext.com/2 )、Visual Studio Code ( https://code.visualstudio.com )或 Atom ( https://atom.io ),所有这些都可以在 Windows、macOS 和 Linux 上运行——并添加以下内容:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 02, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
    <p>Hello World</p>
  </body>
</html>

这只是一个基本的 HTML 页面,没有什么花哨的。看看这两个<script>标签

<script src="jquery.js"></script>
<script src="app.js"></script>

先载入 jQuery 再载入app.js文件,目前为空。

Note

使用script标签加载文件的顺序非常重要。请记住,您编写的代码将依赖于 jQuery,因此您必须在任何使用它的脚本之前加载 jQuery。

现在你有了自己的页面,继续在浏览器中加载index.html。除了“Hello World”文本之外,您还看不到任何内容。进入app.js并添加下面一行——你在书中写的 jQuery 的第一行!

$("body").css("background", "red");

你能猜到这是干什么的吗?你已经看到了$("body")选择了"body"标签(记住,它们只是 CSS 选择器),你可能会尝试一下css("background", "red")做什么。刷新页面,你…不会看到任何变化。

这是许多 jQuery 初学者在开始时都会犯的错误。问题又回到了您的index.html文件中:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8' />
    <title>Chapter 02, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
    <p>Hello World</p>
  </body>
</html>

在加载页面的其余部分之前,先加载 JavaScript,所以当执行 JavaScript 时,页面还没有完全加载,这意味着 DOM 还没有准备好。因为在运行 JavaScript 时页面没有完全加载,所以浏览器没有完成 DOM 的构建,这意味着就 DOM 而言,在运行 JavaScript 时,"body"并不存在。您有两种选择:

  • 在页面底部包含您的 JavaScript,就在关闭</body>之前。这意味着它在 DOM 加载后运行。
  • 告诉您的 JavaScript 在 DOM 准备好之前不要执行。

实际上,最好在底部包含 JavaScript,这样就不会延迟内容加载。所以在这本书的大部分时间里,我们都会这么做。然而,这一次,我们将选择第二个选项——纯粹是因为我们需要解释你如何着手去做。为了讨论如何在 DOM 加载之前停止代码的运行,我们将简单讨论一下事件。我们将在第四章中详细介绍这些事件,但是你现在需要尝试一下这个话题。

在浏览器中,编写 JavaScript 是非常基于事件的。编写基于事件执行的代码。用户单击一个按钮,向下滚动页面,悬停在一个图像上,等等。这些动作中的每一个都会引发一个事件,这个事件被 JavaScript 捕获,然后基于事件的发生执行代码。

加载 DOM 时,浏览器也会发出一个事件。然后,您可以编写仅在该事件触发时执行的代码,这意味着您知道您的代码将仅在 DOM 设置完毕并准备就绪时执行。使用 jQuery,您可以这样做:

$(function() {
  //DOM is ready to go
});

让我们来分解这条线:

  • $(document):这将变量document传递给 jQuery。document变量是一个特殊的变量,它包含对页面上所有 HTML 元素的引用。当这个对象触发一个ready事件时,您想要执行一些代码。

  • .ready() : ready是 jQuery 支持的众多事件之一。你传递给它一个函数,这个函数在ready事件被触发时被执行。因为你正在做$(document).ready(),当一个ready事件在document对象上注册时,你传入的函数被触发。

  • function() {}:您传递给ready调用的是一个常规的 JavaScript 函数,当事件发出时,它将被调用。这就像创建函数,就像你在第一章中所做的那样,但是你没有命名它们,而是直接传入一个函数。你可以这样做:

    function onReady() {
      alert("READY TO GO!");
    }
    $(document).ready(onReady);
    
    
  • 但在实践中,更简单的方法是创建函数并立即将其传递给事件处理程序,而不需要先显式命名它。这样,你就创建了一个匿名函数,一个没有名字的函数。

前面一行代表以下所有内容:

$(function() {
  $("body").css("background", "red");
});

当你刷新index.html时,你会看到一个红色的背景!现在,做$(document).ready(function() {})是如此普遍,jQuery 有一个方便的快捷方式。您可以简单地执行以下操作:

$(function() {
});

这意味着完全相同的事情。如果 jQuery 检测到您已经向它传递了一个函数,它会认为这个函数应该已经在 DOM 上执行了。这是一个方便的小快捷方式,可以帮你节省少量的打字时间。

上一段中的短语“jQuery 检测到您已经向它传递了一个函数”意味着当您选择类似于$("body");的东西时,您实际上正在做的是调用 jQuery 提供的一个函数,该函数被存储为一个名为$符号的变量。你也可以轻松做到这一点。下面的代码是有效的,因为 JavaScript 不介意变量中有$符号或者变量名只有一个字符长:

var $  = function() { console.log("hey"); };
$(); //logs "hey" to the console

Note

如果页面上有 jQuery,就不要这样做,因为您将覆盖$变量——这意味着它将不再引用 jQuery。

所以 jQuery 所做的就是将一个函数绑定到$,这很聪明。它可以检测您传递给它的内容,并执行某些操作。所以当你传入$("body");时,它知道选择 body 元素。但是当你传入$(function() {}) ;时,它会检测到你传入了一个函数,并相应地采取行动。

现在,尝试一些更复杂的东西。在 HTML 页面的<head>中,添加一个到新样式表的链接,您也应该创建这个样式表,如下所示:

<link rel="stylesheet" type="text/css" href="style.css" />

删除 Hello World 段落,并在<body>标记中用以下 HTML 替换它:

<div id="box">my box</div>

进入style.css并添加以下内容:

#box {
  width: 100px;
  height: 100px;
  text-align: center;
  background: #f00;
  font-size: 14px;
}

最后,将您的app.js编辑成简单的:

$(function() {
});

您应该会看到一个简单的屏幕,如图 2-3 所示。

img/A310335_2_En_2_Fig3_HTML.jpg)

图 2-3。

The resulting box

它不会赢得任何设计奖项,但现在你可以用这个盒子做一些有趣的东西。首先,创建一个变量来存储对 ID 为"box"div的引用,如下所示:

var box = $("#box");

将对它的引用保存为变量,因为您将不止一次使用它。执行以下操作是低效的,因为每次使用$("#box")时,都要让 jQuery 选择元素两次:

$("#box").doSomething();
$("#box").doSomethingElse();

最好只做一次,然后保存到一个变量中。

动画示例

现在让我们看一个 jQuery 动画示例。虽然动画可能看起来令人望而生畏,特别是在开始时,但它是真正展示 jQuery 能力并立即给出结果的领域之一,这使得它成为一个很好的起点。这个例子不会涉及太多的细节,只是浏览一下 jQuery 的一些关键特性。稍后,您将详细了解每个领域。这纯粹是对 jQuery 功能的简单介绍。

你要做的第一件事是淡出你的盒子。使您的app.js文件看起来如下:

$(function() {
  var box = $("#box");
  box.fadeOut("slow");
});

刷新你的页面——那个可爱的红框会慢慢淡出你的视线。很容易理解为什么fadeOut()方法被恰当地命名。正如你所看到的,传入参数"slow"使盒子淡出得更慢。你也可以使用关键字"normal""fast",它们的功能和你想象的完全一样。

如果您想要一个全面概述fadeOut方法如何工作以及如何使用它的页面,请查阅 jQuery API 文档。如果您搜索 fadeOut 并找到该方法的文档,您会看到如图 2-4 所示的内容。

img/A310335_2_En_2_Fig4_HTML.jpg)

图 2-4。

The jQuery documentation for the fadeOut() method

文档的第一部分如下:

.fadeOut( [duration] [, complete] )
duration A string or number determining how long the animation will run.
complete A function to call once the animation is complete.

能够阅读和理解 API 将为您节省大量时间。前面的语法现在可能还很陌生,但是一旦你知道它是如何工作的,就很容易理解了,因为它在整个 API 中都是一致的。第一行描述了如何调用该方法。这表明您可以通过传入一个持续时间和一个完成函数来调用fadeOut()。每个参数周围的方括号表示该参数是可选的,您不必传递任何一个参数。您可以传入一个参数,传入两个参数,或者一个都不传,jQuery 知道如何处理这种情况。之前,您这样称呼它:

$("#box").fadeOut("slow");

您可以看到您传入了持续时间,但没有传入完成方法。回调是 JavaScript 中经常使用的一个术语,指的是一旦某个东西执行完毕就被调用的函数。在fadeOut()的上下文中,这个完成函数会在你的盒子淡出后被调用。要查看实际效果,请将您的app.js更改为以下内容:

$(function() {
  var box = $("#box");
  box.fadeOut("slow", function() {
    alert("box finished fading out");
  });
});

一旦该框淡出,您将在屏幕上看到一个警告。这给了你很大的力量去做一些事情,然后在最初的事情完成的时候运行其他的事情。回调在 jQuery 中被广泛使用。大量的方法,尤其是动画方法,都需要回调,你会经常用到它们。当然,因为两个参数都是可选的,所以您也可以只传入一个回调,就像这样:

$(function() {
  var box = $("#box");
  box.fadeOut(function() {
    alert("box finished fading out");
  });
});

你会注意到当你刷新的时候,这个盒子会更快的消失。以前,你以“慢”的速度通过。但是如果没有传入一个会怎么样呢?jQuery 是做什么的?

任何可选的参数都有一个默认值,jQuery API 会告诉你它是什么。以fadeOut()为例,jQuery API 说:

Duration is given in milliseconds; Higher values indicate slower animations, not faster ones. You can provide the strings "fast" and "slow" to represent the durations of 200 and 600 milliseconds, respectively. If any other string is provided or the duration parameter is omitted, the default duration of 400 milliseconds is used. ( http://api.jquery.com/fadeOut/ )

所以如果你遗漏了一个参数,它默认为 400 毫秒。传入“slow”是将它设置为 600 毫秒。你也可以传入一个数字。试着慢慢来。记住,数字是毫秒,所以 3 秒= 3000 毫秒。

$(function() {
  var box = $("#box");
  box.fadeOut(3000, function() {
    alert("box finished fading out");
  });
});

所以有了fadeOut(),jQuery 就有了它能识别的三个默认字符串:

  • "slow" : 600 毫秒
  • "normal" : 400 毫秒(也是默认)
  • "fast" : 200 毫秒

除此之外,您可以以毫秒为单位传入一个值。

现在假设您想要连续地淡入淡出一个框,可能需要十次。你不会惊讶地发现fadeOut()有一个同伴fadeIn(),它做的正好相反。所以,你可以把这两种方法结合起来,得到想要的效果。至少,如果您没有正确地浏览 API,您会这样做。

你看,除了fadeIn()fadeOut(),还有fadeToggle()。如果它不可见,它将在框中淡入,如果它可见,它将淡出框。因此,你可以利用这一点使事情变得容易得多。如果您在文档中搜索了“fade ”,那么您已经看到了这个方法。我们怎么鼓励你广泛使用 API 文档都不为过,尤其是在学习的时候。

img/A310335_2_En_2_Fig5_HTML.jpg)

图 2-5。

The API documentation search results for “fade”

所以,你要做的是:

  1. 创建一个函数,将切换框,然后退出。
  2. 存储一个变量,记录你这样做的次数。
  3. 有一个段落,它的文本总是更新为框淡入淡出的次数。

然后,该函数将调用自身,使该框再次淡入淡出——如果保持计数的变量小于某个数量。

这里会有几个新东西,耐心点。最后,您将会看到 jQuery 让事情变得多么简单,这是非常令人兴奋的。你是从上一个练习停止的地方继续,所以你不是完全从头开始。

首先要做的是将段落添加到您的index.html页面,看起来应该如下:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 02, Exercise 02</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div id="box">my box</div>
    <p></p>
  </body>
</html>

添加一个空的 HTML 元素并不是一个好的做法,但是在不久的将来,你将学会如何避免这个问题。编辑app.js,设置将要使用的变量,如下所示:

$(function() {
  var box = $("#box");
  var para = $("p");
  var i = 0;
});

您将存储对框、段落和count变量i的引用,该变量被设置为 0。首先要做的是让你的段落显示计数器的值。要更新元素中的文本,可以使用text()方法。如果您不带参数调用text(),将会返回文本。如果你传入一个参数,它会将文本设置为你传入的内容。因此,你可以做para.text(i);来设置文本的值为i

现在,您必须编写主函数来执行所有的切换。与其一行一行地做这件事,不如直接查看完整的代码,然后遍历它。您的app.js文件将如下所示:

$(function() {
  var box = $("#box");
  var para = $("p");
  var i = 0;

  para.text(i);
  function toggleBox(i) {
    box.fadeToggle(500, function() {
      i = i + 1;
      if(i < 10) {
        para.text(i);
        toggleBox(i);
      };
    });
  };

  toggleBox(i);
});

先说一下toggleBox()功能:

function toggleBox(i) {
    box.fadeToggle(500, function() {
      i = i++;
      if(i < 10) {
        para.text(i);
        toggleBox(i);
      };
    });
  };

你做的第一件事是调用fadeToggle(),它将根据当前状态淡入或淡出盒子。就像fadeIn()fadeOut()一样,给它一个速度——半秒(500 毫秒)——和一个回调函数,一旦盒子淡入/淡出就执行这个函数。该函数采用一个参数i变量,它存储已经执行的渐变次数。你需要这个来看看你是否应该继续褪色。

在回调中,执行以下操作:

  • i的值增加 1-,使用++运算符,这是i = i + 1的快捷方式。
  • 如果i < 10:
    • 将段落的值设置为i的当前值。
    • 再次调用toggleBox(),传入i

这样,你可以刷新你的页面,看到一个在停止前会淡入淡出五次的框。您还会看到显示发生次数的段落。

但是坚持住。为什么它显示 9,而不是 10?事实上,它已经淡入淡出十次了。原因是i最初被设置为 0,所以盒子第一次褪色时,实际上是第零次褪色。因此,当i为 9 时,它实际上发生了十次。

通常的做法是让count变量从 0 开始,主要是因为数组是零索引的,正如你在第一章中看到的。但是,您可能希望输出的值从 1 到 10,这很容易通过更改两行代码来实现

para.text(i);

para.text(i+1);

因此,1–10 将显示在浏览器中,但在幕后,它使用 0–9。

摘要

哇哦。这是艰难的一章,你已经做了很多:

  • 看到了如何下载最新版本的 jQuery。
  • 发现了什么是精简代码,以及为什么您应该总是使用精简的 jQuery 版本。
  • 通过fadeIn()fadeOut()fadeToggle()介绍了一些动画。
  • 使用回调函数在动画完成后运行一段代码。
  • 通过text()方法更新了 DOM 中的文本。
  • 发现了如何通过使用$(document).ready()使代码仅在 DOM 加载后运行。
  • 使用 jQuery API 找到您想要的方法。

如果你感到有点害怕,不要担心。这是 jQuery 所能提供的一些功能的一个短暂停留。下一章通过展示如何遍历 DOM,更系统地介绍了 jQuery 所提供的一切。

三、穿越大教堂

您已经看到了 jQuery 如何工作,以及如何让动画框淡入淡出。现在是时候更有条理地看看这个库,探索它能做的一切了。本章没有涵盖 jQuery 必须提供的每一种方法,因为许多方法做的事情非常相似。也有一些方法做了完全相反的事情。例如,在第二章中,在看了fadeOut()如何工作之后,你只简单地看了一下fadeIn(),因为在见过fadeOut()之后,很明显它会做什么。很多 jQuery 方法也有类似的情况。

然而,这一章不仅仅是所有 jQuery 遍历方法的文档。效率是这一章的一大部分——而且会被多次提到。本章的内容如下:

  • 用 CSS 选择器选择元素并探索哪一个是最有效的。
  • 使用 jQuery 伪选择器。
  • 探索 jQuery 提供的各种遍历方法。
  • 缓存选择器和链接方法以避免重选元素。
  • 避免不必要的 DOM 工作。任何 jQuery 项目的瓶颈总是 DOM 交互。与 DOM 交互是昂贵的,所以你能做的次数越少越好。

jQuery 中的 CSS 选择器

jQuery 的魅力,以及它如此受欢迎的原因,当然与它如此易于使用这一事实有关。您可能熟悉 CSS,并且知道要通过 ID 选择元素,可以使用散列符号(#)。要按类选择元素,可以使用句点(。),等等。jQuery 允许您使用这些选择器(以及更多)从 DOM 中选择元素。同样重要的是,它提供了向后兼容性。所以即使你用的 CSS 选择器在 IE7 及以下不能用,在 jQuery 中用的时候还是可以用的。

然而,伴随着强大的能力而来的是巨大的责任,从计算的角度来看,很多这样的选择器是非常低效的。选择元素的最基本方法是按其 ID,如下所示:

$("#header");
$("#maincontent");

这种方式总是比通过类或标签名选择更快,其他常见的方式;例如:

$(".column");
$(".header");

$("body");
$("div");

通过 ID 选择是最佳方式的原因有两个。首先,JavaScript 有自己的按 ID 选择的机制—document.getElementById("header")—所以当 jQuery 检测到您传入了一个 ID 时,它可以简单地调用那个方法。第二,应该只有一个元素具有特定的 ID,所以一旦找到结果,它就停止搜索。

Note

由您来确保一个页面上只有一个 ID 实例。如果有多个元素具有相同的 ID,JavaScript(以及 jQuery)将只返回第一个元素。一个 id 存在于多个元素上是无效的 HTML。

如果你通过一个类寻找某个东西,可能会有多个结果,所以 JavaScript 必须继续搜索整个 DOM。如果可以通过 ID 选择元素,那么就这样做。

另一件值得一提的事情是 jQuery 处理选择器结果的方式。无论返回一个元素还是五十个元素,结果都将返回一个类似数组的结构(它实际上不是一个数组,但很快会有更详细的描述)。假设你在一页上有一个段落,你运行$("p")。看看你得到了什么:

[<p>Hey</p>]

如果你还有几个,你会得到这个:

[<p>Hey</p>, <p>Hey</p>, <p>Hey</p>, <p>Hey</p>]

Note

如果你使用的是谷歌浏览器以外的浏览器,你的输出可能会略有不同。前面的示例显示了 Chrome 浏览器开发工具的输出。例如,在 Firefox 中,输出是

[p, p, p]

请放心,代码会找到相同的元素集。只是控制台输出的方式不同而已。

这样做的好处之一是,您可以通过对结果使用.length来轻松地找出返回的项目数,如下所示,因为结果的行为就像一个 JavaScript 数组:

$("p").length; // 4

在 jQuery 1.8 之前的版本中可以使用 jQuery 方法$("p").size(),但是.size()所做的只是返回使用.length的结果,所以开发人员通常使用.length

在这个阶段,jQuery 可能看起来只是返回了一个常规数组,但事实并非如此。它返回一个 jQuery 对象。这个 jQuery 对象就像你在第一章 1 中探索的常规对象一样。它包含所有 jQuery 属性和方法,以及您执行的选择器中的元素。一种很好的理解方式是,jQuery 对象是一个增强的数组。在其核心,它有一个 DOM 元素列表——但远不止这些。请记住,当您运行$("p")并返回看起来非常像数组的内容时,它并不是。它实际上是一个 jQuery 对象。

jQuery 的新用户发现最令人困惑的事情之一是,一些方法在它们返回的每个元素上都被调用,而另一些则没有。例如,假设你有一个包含四个段落的列表,你想给每个段落一个类。以下将起作用:

$("p").addClass("paragraph");

addClass()方法非常简单明了。它只是向元素添加了一个类。注意,这个addClass()方法在结果集中的每个元素上运行。还要注意的是,您不必循环遍历它们。如果您有一组元素并调用一个方法,jQuery 通常会隐式地为您执行循环。这真的很有用,但是可能有点混乱,所以只要记住 jQuery 会尽可能地为您循环。

当然,因为 jQuery 可以解析 CSS 选择器,所以可以给它传递非常复杂的选择器,比如:

$("div>ul a");
$("div#main p strong");
$("div.main p>li a");

但是这些选择器的缺点是它们越复杂,运行的时间就越长,代码执行的速度就越慢。jQuery 从右到左解析它的 CSS 选择器,所以最后一个例子做的是

  • 定位所有定位元素。
  • 过滤掉不在列表项中的锚元素。
  • 过滤掉所有剩余的元素,这样剩下的元素就在一个段落的直接子元素<li>中。
  • 仅选择类别main中的剩余元素。
  • 仅选择那些在具有该类别maindiv内的剩余部分。

光是找到一些链接就要做很多工作。在决定使用哪个选择器时,您需要警惕并牢记这种事情。

遍历方法

遍历方法是让我们在 DOM 中“跳”来寻找特定元素的方法。遍历方法将帮助您通过多种方式从元素 A 到达元素 B。在本节中,您将研究这些方法是什么,并探索如何尽可能提高效率。jQuery 有大量的遍历方法,正如文档( http://api.jquery.com/category/traversing/ )将向您展示的那样。

在这一章的剩余部分,我们将讨论我们认为最有用的方法——也是你最常用的方法。一路上会有各种切线,进一步讨论你需要知道的东西。

您通常会有一组想要缩小范围的元素。也许你只想要第一个,或者最后一个,或者你想从数组中选择一个特定的。您可以使用eq()方法来实现这一点。假设您的 HTML 包含一些段落,如下所示:

<p>Para 1</p>
<p>Para 2</p>
<p>Para 3</p>

那我们假设你跑了$("p")。您将得到以下结果:

[<p>Para 1</p>, <p>Para 2</p>, <p>Para 3</p>]

eq()方法将返回一个 jQuery 对象,其中包含特定索引处的元素。例如,$("p").eq(0)将给出包含第一段的 jQuery 对象(记住,数组是零索引的)。假设您要运行以下内容:

alert($("p").eq(0).text());

您会看到Para 1,因为获取第一个元素是如此常见的事情,以至于 jQuery 提供了first(),它做的完全一样。得知还有一种last()方法,你不会感到惊讶。

因为获得第一个结果是如此常见,jQuery 给了我们另一种做事方式。请考虑以下几点:

$("p:first");
$("p:eq(0)");

这两者会达到同样的效果。jQuery 支持许多这样的伪类。

Note

jQuery 支持大多数 CSS 伪类,但也有一些自己的伪类,比如:eq(0)。你可以在 https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes 找到更多关于伪类的信息

一些伪类直接匹配 CSS3 规范,但是其他的(包括前面两个)不匹配。在本章和本书的过程中,你会看到很多。

现在的问题是,当 jQuery 同时提供伪类选择器和方法时,应该使用哪一个。我们更喜欢使用方法而不是伪选择器。我们认为他们阅读能力更强,更容易明白发生了什么。这是因为当您使用该方法时,它不包含在选择器中。这意味着当您浏览代码时,对该方法的调用更加突出,因为它不在选择器中。

不过,还有另一个原因。更新、更现代的浏览器如 Google Chrome 和 Mozilla Firefox 支持两种非常强大的方法:querySelector()querySelectorAll()。这些是强大的选择方法,可以解析任何 CSS 选择器。querySelector()返回选择器的第一个匹配,querySelectorAll()返回所有匹配。jQuery 将始终使用querySelector()querySelectorAll(),如果它们可用的话,因为这样做可以更快地获得具有复杂选择器的元素。

如果使用$("p:first")选择器,jQuery 不能简单地将"p:first"传递给querySelectorAll()方法,因为":first"不是 CSS 伪类。但是,如果使用$("p").first(),jQuery 可以将"p"选择器传递给一个本地 JavaScript 方法——在本例中为getElementsByTagName()——然后对$("p")的结果调用first()。任何本地方法总是最快的,所以无论何时您可以选择允许 jQuery 使用本地方法,您都应该这样做。

进一步遍历

一旦获得了初始的元素集,很有可能需要在其中进行进一步的搜索。以下面的 HTML 结构为例:

<div>
  <p>Paragraph <strong>one</strong></p>
  <p>Paragraph Two</p>
</div>

假设您首先选择了<div>,然后将其保存到一个变量中(也称为缓存):

var myDiv = $("div");

现在让我们假设您想要查找那个<div>中的所有段落。jQuery 提供了两种实现方式:

myDiv.find("p");
myDiv.children("p");

或者,当然,你本来可以写

$("div p");

综合考虑这三种方法,哪种最好?如果单从速度上来说,$("div p")在新的浏览器中总是最快的——也就是说,那些支持querySelector()querySelectorAll()的浏览器。如果你只为这些浏览器建立一个网站,那么在最初的选择中进行大部分过滤实际上更有效,所以你使用querySelectorAll()

如果你担心老版本和新版本的浏览器,那么$("div p")通常是最慢的方法,特别是当你的选择器更复杂的时候。这样就剩下两种方法:find()children()。这两种方法有一个重要的区别。API 将它们在 http://api.jquery.com/category/traversing/ children()处描述为“获取匹配元素集合中每个元素的子元素,可选地由选择器过滤。”find()被描述为“获取当前匹配元素集中每个元素的后代,由选择器、jQuery 对象或元素过滤。”

关键的区别在于每个描述的第三个词。第一个方法将获得每个元素的子元素,第二个方法获得子元素。看这张图表:

div
- p
- p
- - strong
- - - a

在这里,段落是div的子段落。然而,段落、<strong>和锚都是div的后代。子元素只是直接的后代,而后代则意味着元素中的一切,不管它在哪个层次上。

在这种情况下,请检查以下结构:

<div>
  <p>Paragraph <strong>one</strong></p>
  <p>Paragraph Two</p>
</div>

你应该用children(),而不是find。原因是find()将搜索 DOM 的每一层来尝试找到一个匹配,而children()将只搜索元素的直接层来找到一个匹配。所以,当你只想要直系后代的时候,比如这个例子,children()会更快。诚然,这是一个微小的速度差异,但它只会做你需要它做的事情——而find会做得更多——所以坚持使用children()是有意义的。这还表明您只选择了直系后代,使您的代码读起来更好。

有了children()find()这两个方法,您就不局限于传入一个标签了。它们,以及所有类似的遍历方法,接受任何 CSS 选择器,就像您可能通过$()传递给初始 jQuery 对象的那些一样,比如:

$("div").find("p>strong");

这将返回作为段落直接子元素的所有strong元素,这些段落存在于一个div中。

另一个非常有用的方法是siblings(),如您所料,它获取当前元素的所有兄弟元素。以这个结构为例:

<div>
  <p class="first-paragraph">Paragraph 1</p>
  <p>Paragraph 2</p>
  <p>Paragraph <strong>3</strong></p>
</div>

运行$("div").siblings()不会给你任何结果。这是因为<div>是该级别的唯一元素。要获得第一段的所有兄弟,您可以执行以下操作:

$(".first-paragraph").siblings();

这将给出包含其他两个段落的结果集,但不包含初始段落。如果您想将初始段落添加到元素集合中,这样您就拥有了元素的兄弟元素和原始元素,您可以使用.add(),它可用于将其他元素添加到现有集合中。例如,考虑使用以下内容:

$(".main").add(".paragraphs");

它将留给您一个包含类"main"的元素结果集,以及包含类"paragraphs"的元素结果集。因此,在本例中,您可以执行以下操作:

$(".first-paragraph").siblings().add(".first-paragraph");

但是这不是很有效率。请注意,您运行了选择器两次。这意味着您要搜索 DOM 两次,这一点也不好。幸运的是,jQuery 提供了andSelf(),这是一种更简单的方法。它从上一个选择中获取一组元素,并将其添加到当前选择中:

$(".first-paragraph").siblings().andSelf();

这给出了一个包括兄弟()和初始段落的集合。andSelf()你会发现自己并不经常使用这种方法,但了解这种方法非常有用。使用 DOM 结构,实际上还有另一种方法可以获得第一段的兄弟段落:

$(".first-paragraph").nextAll();

nextAll()获取当前元素之后的所有兄弟元素。因此,对于这个 HTML,如下所示:

<div>
  <p>Paragraph 1</p>
  <p class="second">Paragraph 2</p>
  <p>Paragraph 3</p>
</div>

运行$(".second").nextAll();只会返回一个元素——第三段。运行$(".second").siblings()给出两个元素——第一段和最后一段。因此nextAll()获得 DOM 结构中当前元素之后的所有兄弟元素。还有一个相反的方法,prevAll(),它获取当前元素之前的所有兄弟元素。还有prev()next(),它们获取当前元素旁边的兄弟元素——在prev()的情况下是当前元素之前的兄弟元素,或者在next()的情况下是当前元素之后的兄弟元素。

链接方法

您可能已经注意到,前面的例子将两个方法链接在一起,就像这样

$(".first-paragraph").siblings().andSelf();

这是 jQuery 的关键特性之一。方法可以被一个接一个地调用,并被链接在一起。任何返回 jQuery 对象的方法都可以被链接。要判断一个方法是否返回 jQuery 对象,请查看其 jQuery API 文档的右上角(参见图 3-1 )。

img/A310335_2_En_3_Fig1_HTML.jpg)

图 3-1。

The top-right corner of the documentation shows that this method returns “jQuery”, meaning it can be chained

截图右上角显示该方法返回 jQuery。这意味着该方法可以被链接。

有些方法可以被链接,也可以不被链接,这取决于它们的使用方式。一种这样的方法是你在第 2 ,text()章看到的。如果不带参数调用text(),它将返回元素的文本。但是,如果您向它传递一些文本,它将设置该元素的文本,然后返回一个 jQuery 对象。text()的文档显示了这一点。有两个不同的条目。

第一个是针对text()本身,它返回文本。你可以看到文档表明它返回一个字符串(见图 3-2 )。

img/A310335_2_En_3_Fig2_HTML.jpg)

图 3-2。

This method cannot be chained because it returns a string

然后是text(),它接受一个参数并设置文本。它确实返回了 jQuery 对象(参见图 3-3 )。

img/A310335_2_En_3_Fig3_HTML.jpg)

图 3-3。

When you use .text() to set the text, it returns jQuery , so it can be chained

一般的经验法则是,任何不显式返回除一组元素之外的东西的方法都可以被链接。

链接是避免多次选择元素的好方法,如下所示:

$("div").fadeOut();
$("div").css("color", "red");
$("div").text("hello world");

您可以这样做,而不是那样做并运行$("div")三次:

$("div").fadeOut().css("color", "red").text("hello world");

空白在这里并不重要,所以如果你想把它们分成多行,请随意。我们经常这样做:

$("div")
  .fadeOut()
  .css("color", "red")
  .text("hello world");

注意不要遗漏方法之间的任何点,记住分号只在最后。如果您不喜欢链接,您可能更喜欢缓存选择,正如您已经看到的:

var div = $("div");
div.fadeOut();
div.css("color", "red");
div.text("hello world");

到目前为止,您已经使用了children()find()来进一步遍历 DOM 结构,但是当然也有一些函数可以做完全相反的事情。这些被称为parent()parents()。两者的关键区别在于parent()只在 DOM 中向上一级,而parents()一直向上。您可以在 jQuery 站点上找到这些函数的定义。

从字面上得到一个元素的所有父元素,直到最顶层的元素。以下面的 HTML 结构为例:

<div>   <p><strong>Hello</strong></p> </div>

$("strong").parents()的结果是

[<p>...</p>, <div>...</div>, <body>...</body>,<html>...</html>]

$("strong").parent()的结果是

[<p>...</p>]

因为parents()遍历整个 DOM,所以您几乎总是想要传递给它一些选择器,只是因为对于.parents()来说,返回bodyhtml元素很少有用。然而,有时您可能希望所有的父元素都包含在body元素中,所以您经常需要做的是过滤掉parents()返回的元素集。有两种方法可以做到这一点。第一种是使用 jQuery 过滤器。之前,您使用了:eq选择器和.eq()方法将结果过滤为一个结果。那是 jQuery 的过滤方法之一,API ( http://api.jquery.com/category/traversing/filtering/ )里都有记载。

我们现在感兴趣的方法是not()。我们也将讨论其余的部分——一些在本章,一些在本书的其他地方。not()完全按照您的预期进行操作——过滤结果。如果您想从您的parents()通话中删除bodyhtml元素,就像这样简单:

$("strong").parents().not("html, body");

这将获取所有的父标签并过滤掉htmlbody标签。这是因为not()采用了 CSS 选择器。您实际上是告诉 jQuery 过滤掉与 CSS 选择器"html, body"匹配的元素。当然,这个选择器匹配htmlbody元素。您也可以使用伪类:not,就像您可以使用:eq一样,但是如前所述,使用该方法比伪类更可取(并且更容易阅读),所以这是您将在本书的其余部分看到的。

然而,有一种更好的方法来做你想做的事情,那就是使用parentsUntil()。有了parentsUntil(),你的代码就简单多了。记住,parentsUntil()获取所有元素,但不包括选择器匹配的元素。现在你已经知道了这个方法,你要做的就是下面的事情:

$("strong").parentsUntil("body");

这给出了期望的结果。jQuery 是一个强大的趋势。如果有些事情看起来有点罗嗦,很有可能有更简单的方法来做。

两个非常有用的过滤器是:even:odd过滤器。将它们与采用过滤器并返回通过的过滤器的filter()方法结合起来,您可以轻松地将背景颜色应用于行,使表格看起来有条纹。这里有一个简单的表格可以使用:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 03, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <table>
      <tr><td>Jack</td><td>Franklin</td></tr>
      <tr><td>Stuart</td><td>Robson</td></tr>
      <tr><td>Rob</td><td>Hawkes</td></tr>
      <tr><td>Alex</td><td>Older</td></tr>
    </table>
  </body>
</html>

下面只是给出了表格的一些边框,以便清楚地定义行:

table {
  border-collapse: collapse;
}
tr {
  border-left: 1px solid grey;
  border-right: 1px solid grey;
  border-bottom: 1px solid grey;
}

td {
  padding: 10px;
}

你可以在图 3-4 中看到结果。

img/A310335_2_En_3_Fig4_HTML.jpg)

图 3-4。

The plain table ready to apply the striped effect

下面是app.js文件:

$(function() {
  var rows = $("tr");
  rows.filter(":even").css("background", "red");
  rows.filter(":odd").css("background", "blue");
});

这给出了如图 3-5 所示的结果(当然不会赢得任何设计奖项)。

img/A310335_2_En_3_Fig5_HTML.jpg)

图 3-5。

The table once the code has run

app.js文件做三件非常简单的事情:

  • $("tr")的结果存储到变量rows中。
  • 过滤偶数行并将其涂成红色。
  • 过滤奇数行并将其涂成蓝色。

这是css()方法,但是非常简单。当传递两个参数(一个属性和一个值)时,它将设置集合中元素的 CSS 值。这个非常简单的例子向您展示了 jQuery 过滤器的强大功能。

进一步过滤

如果 jQuery 的内置过滤器还不够,它还提供了一种机制,允许您根据需要进行过滤。当您使用filter("even")过滤偶数行时,您简要地看到了filter()方法的作用。您还可以给filter()传递一个函数,该函数将评估集合中的每个元素,并只返回符合特定条件的元素。

在你的index.html页面中,添加四个段落,看起来像这样:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 03, Exercise 02</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <p><strong>Jack</strong> Franklin</p>
    <p><strong>John</strong> Hammelink</p>
    <p><strong>Richard</strong> Quick</p>
    <p>Will Hammil</p>
  </body>
</html>

摆脱你之前的一切;你不需要任何造型。

现在,让我们说,你想过滤只有段落有一个<strong>标签,并给他们一个红色背景。你要做的第一件事是获取所有的段落,并把它们存储在一个变量中,就像这样:

$(function() {
  var ps = $("p");
});

当你传递给filter()一个函数时,它期望这个函数为每个元素返回truefalse. filter()运行一次,当你传递的函数计算结果为true时,它将保留元素。它将去掉使函数评估为false的元素。

在这个函数中,你可以通过第一章中提到的this关键字访问当前元素。this关键字是 JavaScript 中的一个特殊变量,您可以经常使用它来引用您正在处理的当前项目。要访问您正在处理的但是包装在 jQuery 对象中的当前元素,您可以简单地运行$(this)

要过滤掉所有没有strong元素的元素,您需要检查段落是否包含任何元素。有两条信息可以得到这个结果:

  • 你可以通过$("p").children("strong");得到一个元素内的所有strong元素
  • 通过添加.length可以看到结果集中有多少个元素,比如:$("p").children("strong").length;

因此,对于包含strong元素的段落,必须满足以下条件:

$("p").children("strong").length > 0;

这个表达式可以返回truefalse,它就是你要传递给过滤函数的内容,就像这样:

$(function() {
  var ps = $("p");

  var strongPs = ps.filter(function() {
    return $(this).children("strong").length > 0;
  });

  strongPs.css("background", "red");
});

你得到了想要的结果,如图 3-6 所示。

img/A310335_2_En_3_Fig6_HTML.jpg)

图 3-6。

Three of the four paragraphs, the ones with a inside, are given a red background

我相信你可以想象到,当传递一个函数时,filter()方法是非常强大的。你可以过滤任何你想要的东西,只要你能把它评估为true

有一种方法可以简化代码。filter()方法仍然返回 jQuery 对象,这意味着它可以被链接。这意味着您可以稍微缩短代码,如下所示:

$(function() {
  var ps = $("p");

  ps.filter(function() {
    return $(this).children("strong").length > 0;
  }).css("background", "red");
});

这里你使用了一个ps变量,但是只引用了一次;把它扔掉,这样你就剩下了:

$(function() {
  $("p").filter(function() {
    return $(this).children("strong").length > 0;
  }).css("background", "red");
});

好多了!

摘要

这是迄今为止最紧张的一章,你涵盖了许多新的领域。书中涉及的所有方法都会用到,所以如果有什么你不太确定的,不要担心——会有很多机会用到这些方法。对于 jQuery,很多都是实践,所以我们建议您自己编写一些代码。试试看。记得使用 API 文档——它真的很棒。在下一章,你将开始用 jQuery 操作 DOM。

四、使用 jQuery 操作 DOM

现在,您已经对 jQuery 及其功能有了一定的了解。您知道如何选择元素,如何确保您的代码只在 DOM 加载后运行,以及更多。您还看了一些动画,并通过使用css()方法改变元素的颜色做了一些基本的操作。你可能没有意识到动画是一种操作。之前,您使用fadeIn() / fadeOut()来操作一段时间内元素的不透明度。本章将完全集中在元素的操作上,包括:

  • css()方法改变 CSS 样式
  • 遇到 jQuery 的animate()方法时会有更多的动画
  • 在 DOM 中插入、移除和移动元素
  • attr()编辑元素属性
  • jQuery 提供了无数的操作方法

在您的过程中,您将会有规律的停顿和小的偏离来检查最佳实践。正如在第三章中提到的,DOM 操作通常是网站的一个巨大瓶颈,所以你应该尽量少做。有很多技巧和方法可以限制花在 DOM 上的时间,我们会在你阅读本章的时候提到这些技巧和方法。

第三章还指出,jQuery API 让你很好地掌握了 API 的操作方法( http://api.jquery.com/category/manipulation/ ),所以你可以随时参考。

半铸钢ˌ钢性铸铁(Cast Semi-Steel)

jQuery 的css()方法非常强大。实际上有三种主要的方法可以使用它。第一种是在确定元素的属性值时。只需给它传递一个参数—您想要知道其值的属性:

$("div").css("width");

$("div").css("margin-right");

$("div").css("color");

重要的是要注意,如果你有一组不止一个的元素,并且你调用了css(),你将得到的结果就好像是在第一个元素上调用了css()。另一个重要的注意事项是,你不能使用速记。例如,这是行不通的:

$("div").css("margin");

Note

如果您使用css()来获取宽度,那么您可能想看看 jQuery 的width()innerWidth()outerWidth()方法。虽然css("width")将返回一个类似"200px"的字符串,但是宽度方法总是返回一个整数值。如果您正在执行任何基于宽度的计算,那么从一开始就将其作为一个整数要比获取一个字符串并转换它容易得多。

也可以使用 CSS 来设置值。若要只设置一个值,请将属性和值作为单独的参数传入。你在第三章用过这个。

$("div").css("color", "red");

$("div").css("border", "1px solid red");

更有用的是,css()方法还接受一个键值对对象,该对象将 CSS 属性映射到您想要设置的值。例如:

$("div").css({
  "background" : "red",
  "margin-left": "200px",
  "color": "black"
});

这是设置 CSS 属性更快的方法。然而,如果您发现自己经常这样做,很可能您应该实际创建一个新的 CSS 类来拥有这些属性,然后简单地用 jQuery 将该类添加到元素中。这意味着 jQuery 做的操作更少,因为它只需要添加一个类。要添加一个类,只需使用addClass()方法:

$("div").addClass("column");

还有removeClass():

$("div").removeClass("column");

如果你想检查一个元素是否有一个特定的类,有hasClass():

$("div").hasClass("column");

这将返回truefalse。如果你想给某个东西添加一个类,不管这个元素是否已经有了这个类,你都可以这样做。jQuery 足够聪明,可以为您解决所有这些问题。没有必要这样做:

if( !$("div").hasClass("main") ) {
  $("div").addClass("main");
};

简单的叫addClass()。类似地,在移除类之前,不需要检查元素是否有类。这两种方法都可以接受多个参数:

$("div").addClass("one two three");
$("div").removeClass("four five six");

如果元素没有类,您想添加一个类,但是如果元素有了类,您又想删除同一个类,那么 jQuery 也能满足您的需求:

$("div").toggleClass("main");

如果该集合中的元素具有该类,它们将移除该类;但是如果他们没有,它将被添加。

还有一些事情您可以使用css()方法,因为 jQuery 提供了更好的替代方法。例如,要隐藏某个元素,可以将其 CSS“显示”属性更改为“无”:

$("div").css("display", "none");

然后你可以再展示一次:

$("div").css("display", "block");

但是,如果在您隐藏它之前,它的“显示”属性被设置为“内联”或“内联块”呢?jQuery 通过提供两个方法来解决这个问题:hide()show()。它们的伟大之处在于,当您使用hide()隐藏一个元素时,jQuery 不仅隐藏了它,还记住了它的“显示”属性。然后,当您在该元素上调用show()时,它会将 display 属性设置回之前的状态。因此,要显示和隐藏元素,请按如下方式操作,而不要使用css()方法:

$("div").hide();

$("div").show();

animate()和动画便利方法

你已经发现了动画,但是直到现在,你还没有遇到 jQuery 使用的主要动画函数:animate()。从它的文档来看( http://api.jquery.com/animate/ ),你会认为它非常复杂和难以使用,但实际上,它非常棒。即使你还没遇到过这种方法,但你目前为止用过的动画方法,包括fadeIn()fadeOut(),都用的是animate()。jQuery 提供了这些被称为便利方法的方法,以节省您的输入。下面是 jQuery 源代码中实现fadeIn()的代码:

function (speed, easing, callback) {
    return this.animate(props, speed, easing, callback);
}

它所做的只是传递您传递给animate()方法的参数。如果没有 fade 方法,下面是淡出元素的方法:

$("div").animate({
  "opacity": 0
}, 1000);

这将使div的不透明度在 1000 毫秒或 1 秒内下降到 0。每次输入都会令人沮丧,所以实现了像fadeIn()这样的便利方法来节省您的输入。还有很多更方便的方法,不只是针对动画,一般的也可以。你会在整本书中遇到很多。

animate()的一般用法与css()方法非常相似:它需要一个对象的属性和值来设置。第二个参数是制作属性动画所需的时间。第三个是回调函数,它的工作方式与您在本书前面传递给 fade 方法的函数完全一样。下面的代码片段向animate()传递了三个参数。第一个是键-值对的对象,包含属性和您希望它们最终成为的值;第二个是以毫秒为单位的时间(还是那句话,1 秒= 1000 毫秒);第三个是回调函数。该功能将在动画完成后立即执行。

$("div").animate({
  'width' : 200
}, 2000, function() {
  alert("Div is now 200px wide!");
});

另一种常见的动画是动画显示元素的高度,通常通过“向上”滑动它们来隐藏它们,这样它们的高度为 0,实际上是隐藏的,或者通过“向下”滑动它们来显示高度,从而向用户显示元素。通过将高度设置为 0,您可以有效地隐藏div:

$("div").animate({
  'height' : 0
}, 2000);

但是由于这种情况非常普遍,jQuery 提供了三种方法来使事情变得更简单:

  • slideUp()
  • slideDown()
  • slideToggle()

这些方法通过高度来激活元素。将一个元素的高度设置为 0,创建一个元素在页面上向上滑动的效果,其高度越来越小,直到消失。slideDown()相反,动画元素的高度到一个特定的值。最后,slideToggle()根据调用元素时的状态,向上或向下滑动元素。如果在高度为 0 的元素上调用slideToggle(),它会向下滑动并显示它。如果你在一个可见的元素上调用slideToggle(),它会向上滑动。

现在看一个例子,看看如何使用这些方法。您将使用一小段 CSS 在页面上创建一个“盒子”,然后看看 slide 方法如何影响这个元素。

创建一个新文件夹来存放这些文件,并创建index.htmlapp.jsstyle.css。添加您在之前所有练习中使用的基本 HTML(我们建议您简单地复制并粘贴一个旧练习,然后将其重命名)。你的index.html应该是这样的:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 04, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div id="box">
      <p>A box</p>
    </div>
  </body>
</html>

迅速做出一些造型:

#box {
  width: 200px;
  height: 200px;
  background: red;
}

让你的app.js空白,准备好被一些令人惊叹的动画填充:

$(function() {
});

现在,选择div后,添加对slideUp()的调用:

$(function() {
  $("#box").slideUp(2000);
});

刷新页面,您将看到框在 2 秒钟内滑出视图。记住,slideUp()所做的只是调用animate()方法;这是一条很好的捷径。

现在更改您的app.js文件,使其看起来像这样:

$(function() {
  var i = 0;
  while( i < 10 ) {
    $("#box").slideToggle(2000);
    i++;
  };
});

当你刷新页面时,你会看到框向上、向下、向上滑动,等等。它会滑动十次。前面的代码包含一个在变量i小于 10 时运行的循环。在i初始设置为 0 的情况下,这确保了循环将运行十次。在这个循环中,您调用slideToggle(),如果它可见,它将向上滑动一个框,如果不可见,它将向下滑动一个框。下面的一行是i++,它将i的值增加 1。

属性和特性

为了获取和设置 DOM 元素的属性,jQuery 提供了attr()方法。这就像css()方法一样。做事有三种方式:

  • $("div").attr("id")获取 ID 属性的值

  • $("div").attr("id", "main")将 ID 属性的值设置为“main”

  • $("div").attr({
      "id" : "main",
      "rel" : "one"
    
    

    });一次设置多个属性

但是还有prop(),它处理属性。attr()处理属性。例如,给定一段 HTML,如下所示:

<input type="checkbox" checked="checked" />

那个checked属性表示当页面加载时,复选框是否应该被选中。当用户与它交互时,它不会自我更新。这是通过checked属性来管理的,该属性在用户勾选或取消勾选复选框时更新。这是一个你应该使用prop()而不是attr()的例子。属性是你可以在 HTML 中看到的,而属性是在“幕后”使用的如果你想阅读更多这方面的内容,jQuery 在 http://blog.jquery.com/2011/05/12/jquery-1-6-1-released/ 的博客是一个很好的起点。它对不同之处以及何时应该使用其中一种进行了详尽的解释。

根据我们的经验,绝大多数时候,您可以(也应该)使用attr()。甚至当你在更倾向于使用prop()的地方使用attr()时,attr()也会简单地为你调用prop()。这并不意味着你应该简单地总是使用attr()——使用prop()可以提高速度。在本书中,你会看到应该使用prop()的时候,特别是在本书的后面,当你看 HTML 表单的时候。

当在窗口或文档对象上设置属性时,你应该总是使用prop(),仅仅因为窗口和文档对象不是 HTML 元素——因为它们没有属性。

这里prop()的用法和attr()完全一样,不用演示怎么用了。这一部分的关键是它们之间的区别。这可能是一个令人困惑的差异,需要一段时间才能理解。如果您需要重读这一部分,请不要担心。

文本()和 html()

如果想要更新元素中的一些文本,最好的方法是使用text()方法。像许多其他 jQuery 方法一样,它将在不带参数调用时返回文本的值;但是当用一个参数调用时,它将设置一个元素的文本。例如:

<p>Hello</p>
$("p").text(); //Hello
$("p").text("Hey");
$("p").text(); //Hey
$("p").text("<strong>Hey</strong");
$("p").text(); //<strong>Hey</strong>

当您将 HTML 传递给text()方法时,它会自动为您进行转义。这意味着 jQuery 替换了符号““&lt;”。然后,浏览器将此显示为“收件人”&gt;。这意味着您将看到文本“”,而不是包裹在strong标签中的单词“嘿”。

strong标签被转义,所以它没有被应用。这就是html()的用武之地。html()的工作方式与text()完全相同,但不会对其中的任何 HTML 进行转义。这意味着在前面的例子中,如果你使用html()而不是text(),你会看到它被解释为 HTML,这意味着strong标签没有被转义,因此你会看到单词“嘿”,现在应该是粗体的。

但是,您不应该通过这些方法插入复杂的 HTML。jQuery 提供了无数插入 DOM 的选项,您很快就会看到。

从 DOM 中移除元素

jQuery 有很多从 DOM 中移除元素的方法,你可以在 http://api.jquery.com/category/manipulation/dom-removal/ 找到。

先说看起来最明显的一个:remove()。它从 DOM 中删除了元素集,但也删除了与之相关的任何东西——比如事件。假设您有一些在元素被单击时运行的代码,但是您随后移除该元素并将其重新插入到 DOM 的其他地方。在这种情况下,您必须重新分配该代码,以便在单击该元素时运行。运行remove()完全摆脱了与之相关的任何东西。

使用remove()返回匹配选择器的整个元素集,而不仅仅是它刚刚移除的元素集。这是一个容易犯的错误。给定这个 HTML 结构:

<div>
  <p>hey</p>
</div>

这个 JavaScript:

$(function() {
  var p = $("p").remove();
  console.log(p);
});

记录了以下内容。请注意,这些示例是从 Google Chrome 开发者控制台生成的。如果您使用不同的,您可能会得到不同的输出。例如,对于下一个例子,Firefox 控制台将简单地显示[p]。不要担心这个——这只是每个浏览器如何将结果输出到它的控制台。

[<p>Hey</p>]

这会让你认为它会把它拿走的东西还给你。但是,如果您有这个 HTML:

<div>
  <p>hey</p>

  <p class="remove">hello</p>
</div>

这个 JavaScript:

$(function() {
  var p = $("p").remove(".remove");
  console.log(p);
});

你把这两段都记录下来:

[<p>hey</p>, <p class="remove">hello</p>]

请注意您是如何将选择器传递给remove()方法来过滤结果的。这里我们只从选择中选择了类别为“remove”的元素,$("p")。在这种情况下,我们本可以使用$(".remove").remove()取得同样的成绩。

您可能会问,如果您无法获得刚刚移除的内容,如何将该元素重新插入 DOM,以便将它从 DOM 中的一个位置移动到另一个位置。我们稍后将讨论如何将该元素添加回 DOM。

如果您想从 DOM 中移除一个元素,但不想移除它的关联,那么可以使用detach(),它的工作方式与remove()完全相同,除了它不会移除当元素被点击时运行的代码之类的东西,尽管您已经将元素从 DOM 中移除了。因此,如果你要重新插入用detach()移除的元素,任何事件处理程序仍然会触发。另一方面,remove()完全删除元素和所有事件处理程序。

有时,您可能希望删除元素中的所有内容,而不是元素本身。这就是empty()的用武之地。以之前使用的 HTML 结构为例:

<div>
  <p>hey</p>
  <p class="remove">hello</p>
</div>

运行这段 JavaScript 代码:

$(function() {
  var d = $("div").empty();
  console.log(d);
});

记录以下内容(同样,如果您在不同的浏览器中,您可能会得到略有不同的输出):

[<div></div>]

div被完全清空。关于empty()非常重要的一点是,它还会删除元素中的文本。这是因为,从技术上讲,文本是一个 DOM 节点,而empty()从元素中移除所有节点。

移除元素的最后一个方法是unwrap(),它的操作大致与empty(). empty()相反,获取元素并移除其子元素,而unwrap()获取元素并移除其父元素。鉴于以下情况:

<div>
  <p>hey</p>
</div>

以及下面的 JavaScript:

$(function() {
  $("p").unwrap();
  console.log($("body").html());
});

一旦您调用了unwrap(),您就可以使用html()方法,当不带任何参数调用该方法时,它会返回一个字符串,该字符串是该元素中的 HTML。可以预见的结果是

[<p>Hey</p>]

简单地删除它所调用的元素的父元素。

创建新元素

在开始向 DOM 插入新内容之前,首先需要了解如何创建一个新对象。最简单的方法是创建一个 HTML 字符串。您将看到的大多数插入方法都会欣然接受这一点:

var newParagraph = "<p>Hello</p>";

然而,如果您插入更复杂的结构,这可能会变得非常复杂。像上一个例子一样,用一个字符串做快速加法没有错;但是对于任何更复杂的东西,你应该像这样创建它:

var newDiv = $("<div></div>", {
  "text": "Hello",
  "class": "newDiv",
  "title": "Hello"
});

这是创建复杂元素的最佳方式,因为复杂元素需要设置很多属性。您在一个空元素上调用 jQuery,然后传入一个将属性映射到值的对象。注意,您也可以使用 jQuery 方法。前面的例子表示一个属性“text ”,并给它一个值“Hello ”, jQuery 随后转换这个值,并使用它的text()方法将文本设置为“Hello”。然后,可以将这个新元素保存到一个变量中,这样就可以将它插入到 DOM 中。让我们现在就做吧!

插入到 DOM 中

你终于来了!这需要一段时间,但是您最终可以看到如何将东西放入 DOM。到目前为止,您已经操作了现有的元素并删除了它们,但是没有添加任何东西。jQuery 提供了大量向 DOM 中插入内容的方法,因此您将看到一些最流行的方法。我们还将讨论效率,因为当涉及到 DOM 时,低效地做事在计算上是非常昂贵的。如果您看一下 jQuery 文档,会发现有三类 DOM 插入方法:

  • DOM Insertion,Around:这些方法允许您在现有元素周围插入元素。
  • DOM Insertion,Inside:这些方法允许您在现有元素中插入元素。
  • DOM Insertion,Outside:这些方法允许您在完全独立的现有元素之外插入元素。

我们将讨论每一个类别。有太多的方法来讨论它们,但是像往常一样,我们将选择我们最经常使用的方法和我们看到其他人经常使用的方法。

DOM 插入,周围

在这个部分中只有三种方法。您会记得您曾经看过unwrap()以及它是如何被用来移除一个元素的父元素的。“around”方法都做了与此相反的事情:它们在现有元素周围包装新元素。有三种方法:

  • wrap()
  • wrapAll()
  • wrapInner()

您最常使用的是wrap(),但是其他方法也很有用,所以您将从wrap()开始研究它们。对于所有使用wrap()的例子,您将使用这个 HTML 结构:

<div>
  <p>hey</p>
</div>

最简单的用法是向wrap()传递一个字符串:

$("p").wrap("<div></div>");

这给了你:

<div>
  <div>
    <p>Hey</p>
  </div>
</div>

这在段落元素周围包装了一个新的div元素。

你可以把它缩短一点。当传入一个新的空元素 HTML 字符串时,只需执行以下操作就可以节省一些字符:

$("p").wrap("<div />");

当然,您也可以通过创建新的元素来包装元素,方法是通过传入 HTML 字符串和一个 properties 对象来创建新元素。然后您可以调用wrap(),传入您刚刚创建的新对象。例如:

var newDiv = $("<div />", {
  "class" : "Hello",
  "text": "Hey"
});
$("p").wrap(newDiv);

给你:

<div> <div class="Hello">Hey<p>hey</p></div> </div>

你可以看到你的段落已经用新的div换行了。

做一些类似的事情:它获取集合中的每个元素,并将它们全部包装在新元素中。因此,在前面的例子中,您可以使用wrapAll()来获得相同的效果。如果你有两个段落并调用wrapAll(),它们都被包裹在一个div中。例如:

<div>
  <p>Hey</p>
  <p>Hello</p>
</div>

$("p").wrapAll("<div />");

给你:

<div>
  <div><p>Hey</p><p>Hello</p></div>
</div>

如果您需要在整个元素组周围添加新的包含元素,这将非常有用。

最后一个 wrap 函数是wrapInner(),将每个元素的内容包装在新元素中。例如,前面运行的 HTML 结构

$("p").wrapInner("<strong />");

给你:

<div> <p><strong>Hey</strong></p> <p><strong>Hello</strong></p> </div>

DOM 插入,内部

DOM Insertion,Inside 方法允许您获取 DOM 中的现有元素,并在其中添加元素。jQuery 文档( http://api.jquery.com/category/manipulation/dom-insertion-inside/ )对此做了最好的总结:“这些方法允许我们在现有元素中插入新内容。”

这里有六种方法(您已经见过其中两种):

  • append()
  • appendTo()
  • html()
  • prepend()
  • prependTo()
  • text()

您已经看到并使用了html()text(),所以我们将跳过这一部分。你大概可以猜到prepend()append()(以及“To”版本)做的事情非常相似;这里没有太多要说的。你可能会发现自己经常使用append()prepend(),所以理解这些例子很重要。

非常简单。你给它传递一个新元素,或者一些 HTML,它把它添加到集合中每个元素的末尾。最好用一个例子来解释,假设您有三个空的div元素:

<div></div>
<div></div>
<div></div>

并运行:

var p = $("<p />", {
  "text" : "Hello"
});
$("div").append(p)

因此,DOM 现在看起来像这样,每个div包含一个新段落:

<div><p>Hello</p></div>
<div><p>Hello</p></div>
<div><p>Hello</p></div>

需要注意的是,append()在元素的最后添加内容,在所有其他元素之后。从这里,你大概可以猜到prepend()会在开头插入元素,在所有其他元素之前。取一个已经包含一些内容的div,运行prepend()会在该内容之前插入新元素,而append()会将其放在该内容之后。以前面 HTML 中的这个div为例:

<div><p>Hello</p></div>

如果您随后运行相同的 jQuery 代码片段,但是更改了段落中的文本:

var p = $("<p />", {
  "text" : "Howdy"
});
$("div").append(p)

你会看到:

<div><p>Hello</p><p>Howdy</p></div>

但是如果你跑了:

var p = $("<p />", {
  "text" : "Howdy"
});
$("div").prepend(p)

你会看到:

<div><p>Howdy</p><p>Hello</p></div>

prependTo()appendTo()只是一种不同的代码编写方式。使用append(),您选择元素,然后向其添加内容,而appendTo()执行相反的操作。例如:

<p>Howdy</p>

$("<strong>hello</strong>").appendTo("p")

给你:

<p>Howdy<strong>hello</strong></p>

appendTo()接受一个参数,它可以是任何 CSS 选择器或 jQuery 对象。因此,您可以使用以下代码获得相同的结果:

var p = $("p");
$("<strong>Hello</strong>").appendTo(p);

这些在prependTo中也能很好地工作(这两种方法在这方面完全相同)。例如:

var p = $("p");
$("<strong>Hello</strong>").prependTo(p);

结果如下:

<p><strong>Hello</strong>Howdy</p>

DOM 插入,外部

这些方法允许您在其他元素之外插入内容。这里有四种方法,但实际上只有两种,因为它们是完全相反的,就像prepend()append():

  • after()
  • before()
  • insertAfter()
  • insertBefore()

after()用于在你的集合中的元素后插入内容。例如:

<div><p>Hello</p></div>

$("p").after("<span>Hey</span>");

会给你:

<div><p>Hello</p><span>Hey</span></div>

如果你要做:

$("p").before("<span>Hey</span>");

你会得到:

<div><span>Hey</span><p>Hello</p></div>

从这里开始,根据您在上一节中所做的,您大概可以猜出insertAfter()是做什么的。这只是一种不同的语法:

$("<span>Hey</span>").insertAfter("p");
$("<span>Hey</span>").insertBefore("p");

我们倾向于使用这种语法— prependTo()insertAfter()等等—比其他语法多得多。这主要是因为使用一种方法,其中您想要插入的东西是代码中的“第一个”,您可以创建新元素并更容易地插入它们。例如,这个:

$("<p />", {
  "text": "Hello"
}).insertAfter("p");

比这个好得多:

var p = $("<p />", {
  "text": "Hello"
})
$("p").after(p);

在本书的后续练习中,我们将倾向于使用insertAfter()appendTo()等,但是如果您喜欢这样做,可以随意交换它们。

有效的 DOM 插入

在继续第五章中的事件之前,我们想花点时间讨论一下效率。正如我们已经提到的,DOM 操作是昂贵的。相对于您将要编写的大多数 JavaScript,移除、操作或插入 DOM 元素将是最慢的部分。我们看到人们低效使用它的最常见的例子是当他们在一个循环中插入大量内容时。假设您想要生成一个由 1 到 10 的数字组成的无序列表,并且您决定在一个循环中执行这个操作是一个很好的方法。你可以这样做。为这个练习创建一个新文件夹,并设置通常的结构,一个index.html文件和一个app.js文件,其中只包含

$(function() {
});

喜欢的话可以加个style.css做造型。以下是列出清单的初步尝试:

$(function() {
  var i = 0;
  var newUl = $("<ul />").appendTo("body");
  while( i < 10 ) {
    $("<li />", {
      "text" : i+1
    }).appendTo(newUl);
    i++;
  }
});

以下是步骤:

  1. 创建一个新的无序列表,并将其添加到<body>元素中。
  2. 然后循环十次。在循环中,创建一个新的列表项,并将其添加到无序列表中。
  3. 将文本值设置为 i+1,因此列表项显示为 1–10,而不是 0–9。

您可能认为这很好,但是在这段代码中,您将插入 DOM 次——一次插入列表,一次插入每个列表项。那是一笔巨款。如果你制作无序列表会更好,但是不要把它插入到 DOM 中。然后将每个列表项添加到这个无序列表中,并将整个列表项插入到 DOM 中,将 DOM 插入的数量从 11 个减少到 1 个。这很容易做到:

$(function() {
  var i = 0;
  var newUl = $("<ul />");
  while( i < 10 ) {
    $("<li />", {
      "text" : i+1
    }).appendTo(newUl);
    i++;
  }
  newUl.appendTo("body");
});

这里的关键是,在循环完成之前,不要将无序列表追加到主体中。在将元素添加到 DOM 之前,可以创建一个元素并向其中添加元素。那是做这件事的最好方法。您仍然将每个列表项追加到无序列表中,但是这不是一个 DOM 插入,因为这个无序列表没有被添加到 DOM 中。只是在最后才加上的。在写这篇文章的时候,我们很好奇这会带来多大的不同,我们发现在循环中插入比在末尾插入慢 45%到 60%。当然,在这种情况下,差异要小得多,因为您只插入了十个元素,但是您应该始终致力于生成高效的代码,并且注意像这样的情况是其中的一大部分。

摘要

在一章的篇幅中,涵盖了大量内容。现在,您可以操纵 DOM 了,与一章前相比,您可以做更多的事情。接下来,您将了解事件以及如何编写基于用户交互执行的代码。您还将构建一个手风琴,将事件和 DOM 操作放在一起。

五、事件简介

当您在浏览器中编写 JavaScript 时,您正在编写事件驱动的代码。大部分代码将在发生某些事情时被执行,比如当用户点击一个链接时让内容滑入。尽管您可能没有意识到,但是您已经编写了一些基于事件的代码:

$(function() {
});

如前所述,您已经编写了在文档准备就绪时运行的代码。这是一个要附加代码的事件。它也被称为绑定。出于几个原因,将 JavaScript 保存在单独的文档中并将其绑定到 HTML 文档是个好主意。首先,它使编辑更容易。另一个原因是它防止人们将代码注入 HTML 文档并覆盖您的代码。

到目前为止,您已经绑定了一些在特定事件上运行的代码。在这一章中,你将会看到事件的类型,并且在这一章的最后,使用你的新知识来制作一个手风琴。手风琴是在一个小空间里包含大量信息的好方法。它的工作原理是获取文本段落——每个段落在一个标题下——并且一次只显示文本的一个部分。用户可以通过单击每个标题显示其下方的文本来进行导航。在下一章中,你将会更深入地研究事件(这是一个很大的主题),并改进你在本章中学到的东西。

浏览器中有很多可以绑定的事件。如果您能想到一个事件,那么几乎可以肯定您可以用 jQuery 绑定到该事件。本章介绍以下事件:

  • click:点击按钮等元素
  • hover:通过鼠标与元素交互;在纯 JavaScript 中,称为mouseentermouseleave
  • submit:提交表格
  • 使一件事发生
  • off:删除事件

最受欢迎的是click事件。

你在网上看到的很多老教程会告诉你使用像click()这样的方法将代码绑定到一个点击事件,就像这样:

$("div").click(function() {
  alert("clicked");
});

这是推荐的做法。但是,将代码绑定到事件有了更新的语法。

Note

绑定到事件的函数通常被称为事件处理程序。

当然,像click()hover()等方法仍然可以使用,但是建议使用当前的 API,它的主要特点是只有一个方法——on()——来为您进行事件绑定。

对于学习 jQuery 的人来说,on()方法可能会让人不知所措,尤其是在开始的时候。因为它完成了许多其他方法的工作,所以看起来相当复杂。然而实际上,它并没有你想象的那么糟糕。下面是 jQuery 1.7 版之前绑定点击处理程序的代码,与 1.7 版相比:

$("div").click(function() {
  alert("hello");
});

$("div").on("click", function() {
  alert("hello");
});

这里没有太多的复杂性。不是对所有不同的事件使用单独的方法,而是将事件名称作为第一个参数传入,然后将事件处理程序作为第二个参数传入。

现在来上一堂快速历史课。这种变化的原因很简单:以前有大量的方法,都集中在事件绑定上。有单独的事件处理程序,如click()hover()等等。然后出现了更多通用事件绑定的方法,比如bind()live()delegate()。可以想象,这变得很复杂,需要大量的解释。这些方法仍然存在于 jQuery 中,但是强烈建议您切换到只使用on()。这就是我们在本书中采用的方法。令人难以置信的强大,以至于需要这一章和下一章来完全涵盖你需要知道的关于事件的一切。

热门事件

现在您已经知道了如何绑定事件,是时候研究一些我在日常开发中最常用的事件了。最明显的就是 click 事件,大家已经看到了。这是你最有可能使用的事件。

另一个热门事件是hover。现在,hover实际上不是一个事件,但它是同时绑定两个函数的简写——一个绑定到mouseenter事件,当鼠标悬停在相关元素上时执行,另一个绑定到mouseleave事件,当鼠标停止悬停在元素上时执行。

如果您想绑定到一个hover事件,您可以使用hover()方法,它有两个函数:

$("div").hover(function() {
  alert("hovered in");
}, function() {
  alert("hovered out");
});

如果你想使用新的on()方法,你必须使用mouseentermouseleave事件:

$("div").on("mouseenter", function() {
  alert("hovered over");
}).on("mouseleave", function() {
  alert("hovered out");
});

通过利用链接,您可以在绑定mouseenter函数后立即绑定mouseleave

然而,有很多次你会发现无论用户何时进入或退出,你都想运行代码。例如,您可能经常想要运行一些代码来重置边距、停止动画等等。如果是这种情况,on()允许您将同一个函数绑定到多个事件。只需将它们作为空格分隔的字符串传递给on()方法:

$("div").on("mouseenter mouseleave", function() {
  alert("hovered on or out");
});

您可以一次绑定到任意多个事件:

$("div").on("mouseenter mouseleave click dblclick", function() {
  alert("hovered on or out, clicked or double clicked");
});

jQuery 不关心绑定了多少个事件;它会做到的。很明显,这是不切实际的,而且你并不经常想这么做,但是知道这一点是有好处的。例如,有时您可能希望在鼠标按下或松开时运行相同的代码。

绑定多个事件提出了一个问题:如何知道哪个事件触发了函数?这是一个很好的问题,也是我们将在本章中很快回答的问题。

回到事件的主题,您可能已经注意到,前面的绑定示例只是引入了另一个事件:双击,它被命名为dblclick。现在,您需要知道的重要鼠标事件已经结束了。下一章将讨论我们暂时跳过的几个问题。概括地说,您需要了解的主要鼠标事件有:

  • click
  • mouseenter
  • mouseleave
  • dblclick

jQuery 事件的另一个重要部分是表单事件。jQuery 使得使用 JavaScript 增强表单——比如自定义验证——变得非常简单。为了增加安全性,在服务器端进行验证也很重要。JavaScript 可以帮助确保电子邮件地址的格式正确,但是它不知道数据库中发生了什么。

jQuery 的简单性很大程度上归结于您能够挂钩的事件。主要的是submit,提交表单时触发。您不必将该事件绑定到表单上的提交按钮,而是绑定到表单本身。例如,对于这个 HTML:

<form action="/some/url" method="get">
  <label>Enter your first name: </label>
  <input type="text" name="first_name" >
  <input type="submit" name="submit" value="Submit">
</form>

您可以在提交表单时运行代码,只需绑定到表单上的submit元素:

$("form").on("submit", function() {
  alert("you just submitted the form!");
});

对于处理单个输入的事件,您最常使用的两个事件是focusblur,这两个事件正好相反。当一个元素获得焦点时,触发focus事件。最明显的例子是当用户点击一个输入框或者开始在里面输入的时候。此时,元素获得焦点,并触发focus事件。当用户继续移动,点击另一个元素或者离开那个元素时,就会触发blur方法。想想focusblur在工作方式上有点像mouseentermouseleave。最重要的区别是focusblur可以通过更多方式触发,而不仅仅是通过鼠标。它们也可以在用户浏览表单时通过键盘触发。因此,对于基于活动输入元素触发的事件,永远不要使用mouseentermouseleave。始终使用focusblur

$("input").on("focus", function() {
  alert("you’re focused on an input");
}).on("blur", function() {
  alert("this input just lost focus");
});

与元素互动

当一个元素触发一个事件时,你经常需要做的一件事就是对与之交互的元素执行动作。也许你想在它被点击时隐藏它,或者慢慢淡入或淡出它。在事件处理程序中,您可以通过关键字this访问当前元素。您已经在之前的动画回调中看到过this关键字,它对事件的工作方式是一样的。当事件被触发时,this关键字被绑定到触发事件的元素。

请注意,this关键字并没有设置为包含元素的 jQuery 对象,而只是设置为 DOM 元素引用。要获取 jQuery 引用,只需将其传递给 jQuery 对象:

$("div").on("click", function() {
  alert($(this).attr("class"));
});

如果要多次引用该元素,最好获取对它的 jQuery 引用,然后将其保存到一个变量中:

$("div").on("click", function() {
  var t = $(this);
  alert(t.attr("class"));
});

在这种情况下,我们调用变量t,但是有一些不同的约定。很多人会选择变量名that;其他人选择$this$tself。你怎么称呼它并不重要——只要确保它是明智的和一致的。没有什么比回到代码中发现自己在不同的地方使用了不同的变量名更糟糕的了!

触发事件

有时您可能想要手动触发一个事件。也许您已经有了一个允许用户填写表单的链接,当它被点击时,您想在表单上触发 submit 事件。jQuery 有trigger()方法来为我们做这件事:

 $("a").on("click", function() {
  $("form").trigger("submit");
});

这在某些情况下很有用;然而,如果你发现自己经常这样做,你可能需要重新考虑你的代码。你不应该不断地触发人为事件,但有时它可能是有用的。例如,如果您正在编写一些代码,允许用户单击链接来浏览一组图像,那么使用键盘上的箭头按钮也是一个好主意。因此,您可能希望导航链接在检测到箭头键被单击时触发一个 click 事件。

解除与事件的绑定

正如您有on()用于绑定到事件,您有off()用于从事件解除绑定。最简单的用法是这样的:

$("div").off();

这将解除每个div的所有事件的绑定。您也可以传入一个事件作为第一个参数来取消绑定该类型的所有事件。以下代码解除了所有单击事件与段落的绑定,因此单击段落不会有任何作用:

$("p").on("click", function() {
  alert("click " + this.id);
});

$("p").off("click");

也可以只解除特定函数的绑定。查看下面的代码,看看您是否能弄清楚当您单击一个段落时会发生什么:

$(function() {
  var clickEvent = function() {
    alert("clickEvent");
  };
  $("p").on("click", function() {
    alert("click");
  }).on("click", clickEvent);

  $("p").off("click", clickEvent);
});

你认为以下哪一种情况会发生?

  • 您会得到两个警告:一个说“clickEvent ”,另一个说“click”
  • 你只得到一个提示说“点击”
  • 你只会得到一个提示“点击事件”

如果你猜对了中间的选项,那你就对了。当您将存储在变量clickEvent中的函数绑定到事件时,您可以通过将该函数和事件类型一起传递到off()方法中来解除绑定。

你不会发现自己过于频繁地使用off()方法,但是就像你在本章中看到的许多东西一样,它在有些地方会派上用场。也许你只想允许一个按钮被点击一定的次数,在这种情况下,你可以记录点击的次数,并在计数器达到你所允许的最大值时使用off()

事件对象

前面我们说过,绑定多个事件提出了一个问题:如何知道哪个事件触发了函数?现在你要找出答案了。

每当您将一个事件绑定到一个函数,然后该函数被触发,jQuery 就会传递事件对象。这个对象包含了很多关于事件的信息。要访问它,只需让您的事件处理程序将一个参数作为参数。jQuery 然后将事件对象传递给这个函数,您可以通过您指定的函数应该采用的参数来获取它。例如:

$(function() {
          $("p").on("click", function(event) {
            console.log(event);
          });
        });

如你所见,你不必这样做。如果您对事件对象不感兴趣,就不要添加参数。如果你给一个函数传递一个它不接受的参数,JavaScript 不会给出错误。事件对象包含许多属性。结果在所有浏览器中都是一样的。以下是将其登录到 Google Chrome 控制台时的输出:

altKey: false
attrChange: undefined
attrName: undefined
bubbles: true
button: 0
buttons: undefined
cancelable: true
clientX: 21
clientY: 54
ctrlKey: false
currentTarget: HTMLParagraphElement
data: undefined
delegateTarget: HTMLParagraphElement
eventPhase: 2
fromElement: null
handleObj: Object
isDefaultPrevented: function ba(){return!1}
jQuery18008258051469456404: true
metaKey: false
offsetX: 13
offsetY: 4
originalEvent: MouseEvent
pageX: 21
pageY: 54
relatedNode: undefined
relatedTarget: null
screenX: 21
screenY: 148
shiftKey: false
srcElement: HTMLParagraphElement
target: HTMLParagraphElement
timeStamp: 1348853095547
toElement: HTMLParagraphElement
type: "click"
view: Window
which: 1

那里有太多太多的东西——很多东西你绝大多数时间都不会在意。在下面的代码中,我们挑选了一些值得了解的关键属性。这些属性中有几个你会用到,但是在本书的后面会用到。

还记得之前提出的关于如何找出哪个事件被触发的问题吗?事件对象包含一个type属性,它就是这样做的。这对于将函数绑定到悬停事件很有用:

$(function() {
  $("div").on("hover", function(event) {
    if(event.type === "mouseenter") {
      $(this).css("background", "blue");
    } else {
      $(this).css("background", "red");
    }
  });
});

您知道当您绑定到"hover"时,它将在mouseentermouseleave上触发(“hover”只是 jQuery 提供的一个方便快捷的方式),因此您所要做的就是在事件处理程序中使用一个简单的语句来确定类型。前面的代码将使div在鼠标悬停时变成蓝色,当鼠标离开时变成红色。

您可以使用pageXpageY属性来获取事件触发时鼠标相对于文档窗口左上边缘的位置。

$(function() {
  $("div").on("click", function(event) {
    alert("Your mouse is at X " + event.pageX + " and at Y " + event.pageY);
  });
});

每当点击链接时,就会出现一个警告框,显示鼠标指针的坐标,如图 5-1 所示。

img/A310335_2_En_5_Fig1_HTML.jpg)

图 5-1。

The alert box showing the position of the mouse pointer when the link is clicked

稍后我们将更详细地讨论这些属性。现在,是时候建造一些东西了!

制作手风琴

到目前为止,我们要求您编写的代码都很小,通常用于展示一个小功能。这一次,你将把你在过去几章中学到的东西结合起来,制作一个基本的手风琴。一旦你在下一章更详细地研究了事件,你将再次访问这个代码并改进它。

启动一个新项目并创建通常的结构,一个只包含

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 05, Accordion</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
  </body>
</html>

包括 jQuery 源代码和一个app.js文件,该文件只包含

$(function() {
});

你还需要做一些基本的 CSS 样式,所以添加一个空白的style.css

您将使用标题和段落的基本 HTML 结构,包含在一个div中:

<div id="accordion">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2>Heading 2</h2>
<p>Lorem ipsum dolor sit amet

, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2>Heading 3</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>

你需要做的第一件事是设计你的手风琴。这不是一本关于 CSS 的书,所以细节并不重要,但是你在这里所做的只是用一些简单的规则给 HTML 添加结构:

#accordion {
  width: 500px;
  border: 1px solid black;
}

#accordion h2 {
  padding: 5px;
  margin: 0;
  background: #ddd;
}

#accordion p {
  padding: 0 5px;
}

这给你留下了如图 5-2 所示的图像。

img/A310335_2_En_5_Fig2_HTML.jpg)

图 5-2。

The basic accordion structure, before any JavaScript has been applied

现在你需要隐藏除了第一段以外的所有段落。这可以用 CSS 来完成,但是当处理这样的东西时,您需要确保在关闭 JavaScript 的情况下仍然可以访问内容。因此,您使用 JavaScript 隐藏内容,因此如果用户没有启用它,他或她仍然可以看到内容。

转到app.js,此时它应该只包含将函数绑定到ready事件的代码。首先要做的是初始设置,所以把所有的标题和段落存储在变量中:

var headings = $("h2");
var paragraphs = $("p");

现在你想隐藏除了第一段以外的所有内容。第四章讨论了遍历 DOM,简单提到的方法之一是not(),一种过滤方法。您可以使用它来过滤除第一段之外的所有段落,并隐藏其余段落:

$(function() {
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
});

如果在浏览器中查看,您应该会看到如图 5-3 所示的内容。

img/A310335_2_En_5_Fig3_HTML.jpg)

图 5-3。

The accordion, with only one section showing

现在,您可以处理单击标题时运行的代码。您需要执行以下操作:

  • 隐藏当前可见的段落。
  • 显示紧跟在被点击的标题后面的段落。

但是只有当标题不是当前活动的标题时才应该这样做,否则你将隐藏和显示同一个段落。

当一个标题被点击时,首先要做的是检查它下面的段落是否可见。如果是,那么你不需要做任何事情。它是这样建立的:

headings.on("click", function() {
  var t = $(this);
  if(t.next().is(":visible")) {
    return;
  }
});

绑定 click 处理程序并将$(this)的值存储在变量t中后,您需要一种方法来访问段落。第四章讲述了next()的方法。它用于获取 DOM 中紧跟在当前元素之后的元素。基于你的 HTML,这将总是与被点击的标题相关的段落。然后你需要使用 i s()方法。它被传递一个选择器,在这个例子中是":visible",如果元素匹配选择器,它将返回true,如果不匹配,它将返回false

如果它与选择器匹配,就意味着它是可见的,所以你要做的就是返回。使用return关键字会导致函数在该点停止执行,并且该函数中不会再运行任何代码。如果您在不需要的元素上运行代码,这是停止运行代码的好方法。

如果t.next().is(":visible")返回false,你就知道你需要显示那个段落,隐藏其他段落。在这种情况下,隐藏所有可见段落,然后只显示您需要的段落,要比专门隐藏可见段落容易得多:

$(function() {
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
  headings.on("click", function() {
    var t = $(this);
    if(t.next().is(":visible")) {
      return;
    }
    paragraphs.hide();
    t.next().show();
  });
});

如果你刷新页面并点击标题,你会看到段落出现,其他段落消失。你完蛋了!

实际上,你还没有完全完成,因为你还可以做些改进。在点击处理程序中,你已经引用了t.next()两次。将t.next()保存到一个变量中,然后引用它,这样更简洁:

$(function() {
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
  headings.on("click", function() {
    var t = $(this);
    var tPara = t.next();
    if(tPara.is(":visible")) {
      return;
    }
    paragraphs.hide();
    tPara.show();
  });
});

此外,如果你在这里有一些动画就更好了,所以让段落滑入和滑出视图,而不是只是立即出现和隐藏。这非常简单—只需更改事件处理程序中的最后两行:

$(function() {
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
  headings.on("click", function() {
    var t = $(this);
    var tPara = t.next();
    if(tPara.is(":visible")) {
      return;
    }
    paragraphs.slideUp("normal");
    tPara.slideDown("normal");
  });
});

现在,当你点击标题的时候,你应该得到一些很好的动画段落。祝贺您,您已经构建了您的第一个 jQuery 手风琴!

摘要

那架手风琴并不完美,你很快就会在下一章发现这一点。事件是 jQuery(和 JavaScript)开发如此重要的一部分,以至于我们专门用了两章来介绍它们。在下一章,你会看到手风琴的一些问题。您还将仔细查看事件,包括本章跳过的一些事件,并重写您的手风琴,使其更加简单。

六、更多事件

在上一章结束时,您得到了一个不错的手风琴,这是您着手的 jQuery 的第一个主要部分。您可能没有意识到这一点,但是您编写的代码可以被整理。在本章中,您将通过查看更高级的功能来完善您对事件的了解,包括:

  • 事件委托
  • 事件传播
  • 防止默认行为
  • 创建您自己的活动

一旦您了解了这些特性,您将再次审视您的 accordion,并进行一些重构来改进代码,使其更加高效。第七章将深入研究动画,你将再次使用手风琴的例子来改进其中的动画。但是首先,是时候深入到事件委托中了。

事件委托

想象一下,一个页面上有 100 个段落,你希望每次用户点击其中一个段落时都会发生一些事情。了解了 jQuery 之后,您可能会相当合理地编写如下代码:

$("p").on("click", function() {
  //do something here
});

这可能会很好,但是效率非常低。为什么呢?这段代码让浏览器逐个遍历每个段落,并为其绑定一个事件处理程序。这意味着它必须将 100 个单独的事件处理程序绑定到 100 个单独的段落 100 次。当你写代码的时候,你注意到一个浏览器必须快速连续地多次做某件事,是时候开始思考是否有更好的方法来写代码来避免它。

这段代码还有另一个问题。想象一下,用户可以在您的页面上添加新内容,然后通过一些后端系统保存到您的系统中。这意味着用户可能正在向页面添加新段落,而您仍然希望您之前绑定的事件处理程序能够处理这些新插入的段落。在下面的代码中,如果您向 DOM 中插入一个新段落并单击它,您会看到警告框吗?

$("p").on("click", function() {
  alert("Hello World");
});
// insert new paragraph code here

答案是否定的,点击新段落不会显示提醒。考虑一下为什么会这样。

当您运行$("p")时,它会选择页面上所有当前段落元素。这不是一个“活的”选择器,无论何时插入新内容,它都会更新。它当时选择 DOM 中的元素,但不更新自己。所以现在有两个问题需要解决:

  1. 如何在一个段落被点击的时候运行一个函数,但是在有很多段落的时候仍然高效地绑定它?
  2. 如何使插入到 DOM 中的任何新段落在被点击时也能运行代码呢?

正如您可能已经猜到的,答案是事件委托。这意味着不是将事件单独绑定到每个段落,而是将事件绑定到所有段落的父元素,并让它将事件委托给段落。这听起来比实际更复杂。以下是对其工作原理的解释:

  • click事件被绑定到所有段落的父元素(保持简单,在这个例子中使用 body 元素)。
  • 当 body 元素检测到您绑定的事件类型时(本例中为click),它会检查点击是否发生在段落上。
  • 如果发生了点击,body 元素就会触发。这是授权发生的地方。

这种绑定方式的主要优点是,您必须将一个处理程序绑定到一个元素,并且只需绑定一次。这比你之前做 100 次要好得多。因为这个事件被绑定到一个父元素,而不是段落,这将适用于您插入的任何新段落!

让我们看看你在实践中是如何做到这一点的。像往常一样,建立一个包含 jQuery 和一个空白的app.jsindex.html文件。如果你喜欢,你可以添加一些样式,但那是可选的。在正文中,添加段落。

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 06, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
    <p>Paragraph 3</p>
    <p>Paragraph 4</p>
    <p>Paragraph 5</p>
  </body>
</html>

进入app.js并添加以下内容:

$(function() {
  $("p").on("click", function() {
    alert("Hello World");
  });
  $("<p />", {
    text: "Paragraph 6"
  }).appendTo("body");
});

注意你是如何像上一章那样绑定 click 事件的,然后插入一个新的段落。如果你在浏览器中打开index.html并点击段落,你会看到前五个(最初存在的)都给你警告,但是点击第六个(你在绑定 click 事件后插入的段落)不会给你警告。使用事件委托来解决这个问题。变化非常简单:

$(function() {
  $("body").on("click", "p", function() {
    alert("Hello World");
  });
  $("<p />", {
    text: "Paragraph 6"
  }).appendTo("body");
});

关键的(事实上,唯一的)变化是这一行:

$("body").on("click", "p", function() {...});

以前,当您使用on()方法时,您以如下形式使用它:

$(selector).on(event, function() {...});

以这种形式使用时,事件绑定到选择器;然而,你也可以用这种形式:

$(selector).on(event, delegateSelector, function() {...});

当以这种形式使用它时,事件仍然绑定到选择器,但是它将把它委托给任何匹配它的子元素delegateSelector的元素。如果您进行了更改并在浏览器中刷新页面,单击第六个段落将按预期工作并显示警告。您正在做的是将 click 事件绑定到正文,并告诉它将它委托给其中的所有段落。这意味着它不关心段落第一次出现的时间,只要它在正文中并被点击,事件就会被触发。

在决定何时授权时,你不应该越级行事。这里的关键规则是常识:如果您将一个事件只绑定到一个元素,委托是没有意义的,因为您不会获得任何东西。我们推荐的规则是,当你绑定到多于几个元素时,不要委托,也许是任何多于五个的元素。事实上,如果你不授权,这并没有多大关系。对于少量的链接来说,性能提升是很小的,但是这仍然是你应该做的事情。即使你做的优化很小,但仍然值得去做。

事件传播

事件传播可能会给刚接触 JavaScript 事件的人带来一些麻烦,所以在深入研究之前最好用一个例子来解释一下。想象你有一个div,在这个div里面,有一个标题。您希望在点击div时运行一段代码,在点击标题时运行另一段代码。很简单,对吧?看一看。一如既往,你已经得到了你的index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 06, Exercise 02</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div>
      <h5>Hello</h5>
    </div>
  </body>
</html>

这是你的app.js:

$(function() {
});

另外,在index.html中包含 jQuery 源代码,并在style.css中快速添加一点 CSS 来设计样式,以便更容易看到发生了什么:

div {
  width: 500px;
  height: 500px;
  background: red;
  padding: 20px;
}

h5 {
  border: 1px solid black;
  background: white;
  width: 300px;
  font-size: 20px;
  top: 20px;
}

这将产生如图 6-1 所示的结果。

img/A310335_2_En_6_Fig1_HTML.jpg)

图 6-1。

The results of the CSS styling

这给了你一个非常基本的页面。现在编写代码,当您单击标题时提示一条消息,当您单击div时提示一条不同的消息:

$(function() {
  $("h5").on("click", function() {
    alert("header");
  });
  $("div").on("click", function() {
    alert("div");
  });
});

刷新浏览器,点击div。您将看到正确的警报,"div"。现在点击标题。你会看到两个警告弹出——一个是文本“标题”,另一个是"div"。刚刚发生了什么?您猜对了:您刚刚看到了活动中的事件传播。

你可能听说过短语事件冒泡。事件传播与事件冒泡相同;它们只是表示同一事物的两个术语。

当一个事件在浏览器中的一个元素上被触发时,它不只是在那个元素上被触发,而是在它的每个父元素上被触发。当您单击示例中的标题时,您也单击了div。标题在div内,这意味着你不仅点击了标题,还点击了div。您还在div的父对象上注册了一个 click 事件,在本例中是主体。这就是为什么当你点击标题时会出现两个警告框——因为你也点击了div。虽然大多数事件(包括您最常处理的事件)都会传播,但并非所有事件都会传播。DOM events 上的 Wikipedia 页面( https://en.wikipedia.org/wiki/DOM_events )有一个方便的表格,显示所有 DOM 事件以及它们是否传播。

什么时候应该担心事件传播?

通常,唯一需要担心事件传播的时候是将事件同时绑定到元素和元素的父元素的时候。大多数情况下,事件传播是你不必担心的事情——本书的前五章没有提到这一点就证明了这一点。如果它给普通开发人员带来了更多的问题,它会更早出现。

幸运的是,有一种方法可以阻止事件传播。您还记得,前面您通过将事件对象传递到事件处理函数中了解了关于事件的信息,如下所示:

$("div").on("click", function(event) {...});

虽然 event 对象包含大量关于事件的信息,但它也包含您可以使用的方法。其中一种方法是stop Propagation()。下面是 jQuery API ( http://api.jquery.com/event.stopPropagation/ )对此的解释:“阻止事件在 DOM 树中冒泡,阻止任何父处理程序得到事件通知。”

因此,您可以传入一个事件对象,调用stopPropagation(),并解决代码中的问题。看看需要的改变:

$(function() {
  $("h5").on("click", function(event) {
    alert("header");
    event.stopPropagation();
  });
  $("div").on("click", function() {
    alert("div");
  });
});

当您现在单击标题时,您将只看到一个包含文本“标题”的警报。

仅仅因为你能做一些事情,并不意味着你应该做——在防止传播方面也是如此。除非事件的传播导致了问题,否则不要阻止它。实际上,传播很少引起任何问题。

防止默认行为

有时,当您绑定到一个事件时,您需要阻止浏览器执行附加到该事件的默认操作。例如,当单击锚元素时,默认的浏览器行为是跟随该链接。有时你会想要覆盖它。也许您希望链接出现在弹出窗口中,所以决定绑定到事件并实现您的弹出代码。让我们看看你会怎么做。这里有一个带有链接的index.html文件:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 06, Exercise 04</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div>
      <a href="http://www.apress.com">Apress</a>
    </div>
  </body>
</html>

这个链接在一个div中,作为一个简化的例子,假设当这个链接被点击时,你希望div的背景变成蓝色,然后什么也不发生。第一次尝试可能是这样的:

$(function() {
  $("a").on("click", function() {
    $("div").css("background", "blue");
  });
});

如果你在浏览器中尝试这样做,你会看到div在你被带到一个新闻网站之前的一瞬间变成蓝色。因此,当它执行您的代码时,它会立即将用户带到另一个站点,使您的代码变得无用。

在事件对象上,除了stopPropagation()之外,还有preventDefault(),您可能会知道它是做什么的。您可以像使用stopPropagation()一样使用它——向事件处理程序传递一个事件对象,然后在该对象上调用preventDefault():

$(function() {
  $("a").on("click", function(event) {
    event.preventDefault();
    $("div").css("background", "blue");
  });
});

重要的是要注意,在事件处理程序中的什么地方调用preventDefault()并不重要(对于stopPropagation()也是如此)。有些人喜欢叫它结尾,有些人喜欢叫它开头,有些人喜欢叫它中间。通常是 put 和 end,我们可以解释为什么。

如果您在事件处理程序的最开始调用preventDefault(),它会立即阻止浏览器的默认动作发生。如果事件处理程序中的某些其他代码导致错误,则发生了两件事:

  • 默认浏览器事件没有触发,因为您在第一行调用了preventDefault()
  • 您绑定到事件的 JavaScript 没有触发,因为有一个错误。

现在假设您在最后调用了preventDefault(),并且事件处理函数中的一些 JavaScript 出错。

  • 因为有一个错误,您的 JavaScript 不会启动。
  • 这个错误意味着preventDefault()没有被调用,所以浏览器的默认行为会发生。

当您的 JavaScript 出现错误时,让浏览器的默认行为发生通常是一件好事——它不会让用户的浏览器完全崩溃(或者至少,在他们看来不会这样)。

关于返回 false 的说明;

在很多教程中,你会看到return false;在处理程序中的使用:

$(function() {
  $("a").on("click", function() {
    $("div").css("background", "blue");
    return false;
  });
});

让处理程序返回布尔值false具有阻止默认事件动作被调用和阻止事件传播的效果。本质上,它实际上是调用stopPropagation()preventDefault()的快捷方式。如前所述,大多数时候你实际上不想调用stopPropagation(),所以我们强烈建议避免使用return false,而使用preventDefault()

您自己的活动

jQuery 事件的一个很少使用但非常有用的特性是能够触发并绑定到您自己的定制事件。你可能在读这篇文章的时候会想,“为什么?”。本节解释了为什么您想要使用这个特性。

当一个复杂的网站有多个事件触发时,您的代码会变得有些混乱。假设当用户单击一个按钮时,您必须执行一些不同的操作。也许你不得不更新标题,改变背景颜色,和其他一些事情。您可以将所有这些添加到这个按钮的 click 事件的一个事件处理程序中,但是很快事情就会变得混乱。然后你意识到这些功能中的一个——也许是改变背景颜色——需要在用户点击按钮或者悬停在某个元素上时发生。从这里开始,你的代码会很快变得混乱。您不能将相同的代码复制并粘贴到两个不同的事件处理程序中,因为那样做太草率了。你可以将代码放入一个函数中,这不会太糟糕。或者,您可以创建一个自定义活动,这样可以两全其美。如果你仍然不相信,坚持住,希望下面的例子能说服你。

和往常一样,为这个例子创建一个新文件夹,它有一个index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 06, Exercise 05</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div>
      <h5>Click Me</h5>
      <h5>Or Me</h5>
    </div>
  </body>
</html>

创建一个空的app.js文件和一个可以添加基本样式的样式表:

div {
  width: 500px;
  height: 500px;
  background-color: red;
  padding: 20px;
}

h5 {
  border: 1px solid black;
  background: white;
  width: 300px;
  font-size: 20px;
  top: 20px;
}

它还拥有 jQuery 源代码的本地副本。在 CSS 样式化之后,你的页面看起来就像图 6-2 所示。

img/A310335_2_En_6_Fig2_HTML.jpg)

图 6-2。

The page with some basic CSS styling

现在想象一下,无论何时单击这些标题,您都需要更改div的背景颜色。当单击这些标题时,以下内容会触发一个自定义事件:

$(function() {
  $("h5").on("click", function() {
    $("div").trigger("bgchange");
  });
});

一个事件必须在一个元素上被触发,所以它在div上被触发。现在,您可以像处理任何其他事件一样,将函数绑定到该事件:

$(function() {
  $("h5").on("click", function() {
    $("div").trigger("bgchange");
  });

  $("div").on("bgchange", function() {
    var t = $(this);
    t.css("background-color", "blue");
  });
});

自定义事件的美妙之处在于,它们为您提供了一种简洁的方式来打包您的代码,并尽可能地保持代码的独立性。如果你的代码可以完全由事件驱动,这是一件好事。与其让大量代码与其他函数交互,不如简单地触发并绑定到自定义事件。这会让你的生活更轻松。它还允许您为您创建的事件指定有意义的名称,从而使您的代码易于跟踪和维护。

手风琴,第二拍

在本章所学知识的基础上,是时候重温你为手风琴编写的 JavaScript 了,看看你是否能改进它。又来了:

$(function() {
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
  headings.on("click", function() {
    var t = $(this);
    var tPara = t.next();
    if(tPara.is(":visible")) {
      return;
    }
    paragraphs.slideUp("normal");
    tPara.slideDown("normal");
  });
});

鉴于这个手风琴可以发展成比你现在拥有的三个标题更多的标题,从直接绑定到标题上的点击事件切换到委托:

$(function() {
  var accordion = $("#accordion");
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
  accordion.on("click", "h2", function() {
    var t = $(this);
    var tPara = t.next();
    if(tPara.is(":visible")) {
      return;
    }
    paragraphs.slideUp("normal");
    tPara.slideDown("normal");
  });
});

请注意添加的将折叠保存到变量的行。虽然你只引用它一次,但是你很容易发现自己会再次引用它,所以把它保存到一个变量中没有什么坏处。然后,切换绑定事件的代码行,使用本章介绍的委托语法。

这仍然很好,但点击标题可能不是显示特定段落的唯一方式。接下来,当一个自定义事件在段落上触发时,您将使段落向下滑动,然后使单击标题触发该事件。

编写代码,使段落在检测到事件时向下滑动:

accordion.on("showParagraph", "p", function() {
  paragraphs.slideUp("normal");
  $(this).slideDown("normal");
});

同样,你可以使用委托,就像你处理标题一样。请记住,自定义事件的处理就像常规事件一样。

然后,您可以重写用于单击标题的事件处理程序,如下所示:

accordion.on("click", "h2", function() {
  var t = $(this);
  var tPara = t.next();
  if(!tPara.is(":visible")) {
    tPara.trigger("showParagraph");
  }
});

你会注意到这个现在简单多了,也更容易阅读。在 click 处理程序中,检查!tPara.is(":visible")是否为真(注意开头的感叹号),如果为真,则在需要显示的段落上触发showParagraph事件。这使得您的整个代码如下所示:

$(function() {
  var accordion = $("#accordion");
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
  accordion.on("click", "h2", function() {
    var t = $(this);
    var tPara = t.next();
    if(!tPara.is(":visible")) {
      tPara.trigger("showParagraph");
    }
  });

  accordion.on("showParagraph", "p", function() {
    paragraphs.slideUp("normal");
    $(this).slideDown("normal");
  });
});

这似乎并不容易。事实上,由于委托,您的代码更简单,但也更高效,并且很容易添加其他方法来触发正确的段落向下滑动。如果你想触发一个段落从另一个方法向下滑动,你所要做的就是触发它上面的事件。它简单且可扩展。

摘要

您应该对在 jQuery 中处理事件相当熟悉。这很好,因为在任何 jQuery 项目中都经常用到它们,无论是在本书中还是在现实生活中。您的 accordion 现在更好了,您能够防止默认行为,您对事件传播有了很好的理解,您甚至可以触发自己的事件。手风琴现在更适合它了。

然而,它仍然不完美。在第七章中,你将进一步深入到动画中,这将突出手风琴的一些问题。你还将探索更复杂的动画领域,并在本章结束时,重温手风琴,以改善它。

七、动画

到目前为止,动画的主题已经在本书中多次提到,但只是非常基础的部分。你已经做了一些褪色和滑动,但仅此而已-直到现在。jQuery 有一个全功能的动画库,但是它也有自己的怪癖:有时事情并不完全像你预期的那样发生。本章将涵盖这些“疑难杂症”以及更多内容,包括:

  • jQuery 的animate()方法,它允许您将大量属性制作成动画。
  • 更多的 jQuery 便利方法,比如fadeOut()slideIn()等等。
  • jQuery 的动画队列,它决定了动画运行的方式和时间。它们并不总是像你想象的那样运行。
  • 初学者在制作动画时常犯的错误——以及如何避免这些错误。
  • 增强你的手风琴的动画效果,使它不那么容易出错。
  • 臭名昭著的 jQuery 项目:图像滑块。

这将是相当沉重的一章。图像滑块将结合到目前为止你所学的一切。我们开始吧!

animate()方法

animate()方法可用于在一段时间内动态显示元素的许多属性。

基本用法

jQuery API 对哪些属性可以被动画化有一个简洁的解释:

All animated attributes should be animated into a single numerical value unless otherwise stated; Most non-numeric attributes can't be animated with basic jQuery function (for example, width, height or left can be animated, but background-color can't unless jQuery. Use color plug-ins). Unless otherwise specified, the attribute value is considered as the number of pixels. Where applicable, units em and% can be specified. ( http://api.jquery.com/animate/ )

插件可用于动画复杂的属性,如颜色,但你通常会动画有一个独特的价值,如宽度或高度。animate()的基本用法与css()方法的语法非常相似:

animate({
  property: value,
  property2: value2
}, 500);

通常情况下,这是方法中使用的形式。它接受一个将属性与值相关联的键-值对对象。

第二个参数是持续时间。与便利方法一样,这可以是fastnormalslow——分别是 200、400 和 600 毫秒的快捷方式。否则,以毫秒为单位指定一个值。如果您没有指定持续时间,则默认使用 400 秒(或“正常”)。

你也可以传入另一个参数,一个回调函数,在本书中你已经用过几次了。当处理动画时,回调函数是动画结束后执行的函数。

您可能想知道为什么不能简单地调用animate()然后运行如下代码:

$("div").animate({ "height": 50 }, 500);
$("p").text("animation finished");

这背后的原因归结于 jQuery 如何处理动画。它不只是运行动画,然后移动到下一行。它让动画开始,然后在动画进行过程中移动到下一行——因此需要使用回调。稍后您将对此进行更详细的研究。

添加回调函数非常简单:

animate({
  property: value,
  property2: value2
}, 500, function() {
  console.log("finished");
});

在回调函数中,this的值将引用刚刚被动画化的 DOM 元素。如果您想对该对象使用 jQuery 方法,只需将它传递给 jQuery,如下所示:$(this)

松开

jQuery 中的动画支持缓动函数,该函数指定动画在动画中不同点的运行速度。例如,您可以选择让动画在除了最后几个瞬间之外的所有瞬间都快速移动,在最后几个瞬间,您可以减慢它的速度,以便它慢慢进入最终位置。默认情况下,jQuery 只支持两种缓解方法:linearswing。Swing 是默认的,所以如果你没有指定一个特殊的缓动方法,swing 将被使用。

图 7-1 显示了摆动和直线的区别。上面的线是摆动的,下面的线是线性的。x 轴是时间,y 轴是距离。

img/A310335_2_En_7_Fig1_HTML.jpg)

图 7-1。

A graph showing how the animation progresses by comparing “swing” and “linear”

这个图表摘自詹姆斯·帕多尔塞的 jQuery Easing 说明( https://j11y.io/demos/jquery/easing/ ),这是一个伟大的网站,查看所有不同的缓解效果。你可以看到两者之间有一个微妙的区别:线性以恒定的速度前进,而摆动开始缓慢,然后加速,然后再次减速。

jQuery UI(用户界面)项目中内置了进一步的缓动效果( http://jqueryui.com )。jQuery UI 项目是一组用于公共 UI 组件的 jQuery 插件,但它也包含额外的 jQuery 附加组件,包括一组缓解效果。稍后,您将使用一些额外的缓动函数,但现在,看看如何使用线性函数而不是默认的 swing 来制作动画:

$("div").animate({
  "height": 500
}, 500, "linear");

要使用的缓和方法只是作为第三个参数进入animate()函数。这一点在animate()的 jQuery 文档中有所体现:

animate( properties [, duration] [, easing] [, complete] )

这表明animate()方法最多可以接受四个参数。方括号中的参数是可选的。jQuery 足够聪明,可以判断出哪些参数是传入的,哪些是不传入的。如果您想传入一个回调函数,以及要使用的缓动函数,只需将它作为最后一个参数添加即可:

$("div").animate({
  "height": 500
}, 500, "linear", function() {
  console.log("finished!");
});

传入两个对象

当传入这么多参数时,事情会变得有点混乱,所以您可以为其他参数传入第二个对象:

$("div").animate({
  "height": 500
}, {
  "duration": 500,
  "easing": "linear",
  "complete":  function() { console.log("finished!"); }
});

在第二个对象中,不需要以任何顺序定义参数。按名称传递它们可以更容易地看到发生了什么。有些人喜欢这样,有些人不喜欢。这往往是个人偏好。

动画快捷方式

大多数情况下,您将设置一个值— height, width, opacity的动画,或者其他。我通常将动画设置到特定的高度,也许是为了在用户悬停在链接上时显示额外的信息,或者是为了视觉效果而让内容滑入:

$("div").animate({ "height": 300 }, 500);

但是,通常情况下,您通常会将动画制作到相对于元素原始高度的高度。您可能需要展开元素以显示更多文本,或者如果元素包含允许输入的文本区域,用户可能需要展开文本框以容纳消息。jQuery 让您像这样制作动画:

$("div").animate({ "height": "+=200px" }, 500);

这使得div比开始时多了 200 个像素。你也可以使用"-="来制作比开始时少 200 的动画。

当然,你并不局限于像素动画。jQuery 假设您是按像素制作动画,但是您也可以按 ems、百分比或任何其他有效单位制作动画。只需指定它,就像这样:

$("div").animate({ "height" : "+=10%" }, 500);

现在你对动画方法已经比较熟悉了,让我们看看一些更流行的便捷方法以及它们的动画效果。

更多方便的方法

您已经遇到了最常见的便利方法,但是在您深入研究动画之前,最好先回顾一下它们——这样您就知道它们实际上是做什么的,并且您可以轻松地使用它们。这不会花很长时间,因为大多数方便的方法都遵循相同的模式,并且可以通过两种方式调用它们:

methodName(duration, callback);

methodName(duration, easing, callback);

这三个参数都是可选的。所有即将出现的方法都遵循这种模式,除非另有说明。

Note

方便的方法也设置元素的display属性。例如,当fadeOut()运行时,它将不透明度设置为 0,然后将显示属性设置为"none"。然而animate()不这样做。它只会让你要求它做的事情变得生动,仅此而已。

衰退

fade 方法用于通过动画显示元素的opacity属性来淡化元素。它们如下:

  • fadeIn();
  • fadeOut();
  • fadeToggle();
  • fadeTo();

唯一变化的方法就是fadeTo()。它需要一个额外的必需参数,即淡化元素的不透明度值。这是一个介于 0(透明)和 1(完全不透明)之间的数字。因为不透明度是第二个参数,这意味着还必须提供持续时间:

$("div").fadeTo(500, 0.5);

这有点不直观,可能会让你犯几次错误,但是你很快就会习惯的。

slide 方法反映了 fade 方法,唯一的遗漏是没有匹配的“slideTo”方法。这是因为你很少会想让一个元素的高度既不是 0 也不是它的初始高度。请记住,如果没有一个方便的方法能完全满足您的需求,那么只需使用animate()。这些方法只是围绕着animate()方法的方便的包装器,以便提供通用功能的快捷方式。

  • slideUp();
  • slideDown();
  • slideToggle();

Note

如果您发现自己在滑入/淡入某个元素之前检查它是否可见,使用切换方法可以节省一些工作。

滑动和褪色

jQuery 还有三种很少使用的方法来显示和隐藏元素:

  • show()
  • hide()
  • toggle()

调用时没有任何参数,它们可以立即显示或隐藏一个元素。但是,如果您传入任何参数,它们会变成动画,同时显示宽度、高度和不透明度。这些方法采用与 slide 和 fade 方法相同的参数:持续时间和回调,或者持续时间、缓动和回调。

动画队列

当您在单个元素上运行多个动画时,它们不会同时运行,而是被添加到 jQuery 的动画队列中。您可以通过一个示例看到这一点。

创建一个新的index.html文件,并填入以下内容:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 07, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div id="box">
      box!
    </div>
  </body>
</html>

设计div的样式,以便更容易看到正在发生的事情:

#box {
  width: 500px;
  height: 500px;
  background: blue;
}

然后将以下代码添加到app.js:

$(function() {
  $("div")
    .animate({ "height" : 300 })
    .fadeOut()
    .show(500)
    .animate({ "width" : 100 })
    .css("background", "red");
});

在浏览器中打开index.html。在动画结束之前,你会看到div的背景变成红色。这是因为 jQuery 将动画一个接一个地排队运行。

浏览器只有一个线程,这意味着它一次只能运行一位代码。多线程应用能够在不同时间运行多个代码块,这意味着它们可以同时做多件事情。多线程允许任务异步执行,而不是同步执行。在浏览器中,这是不可能的。如果一段代码运行了很长时间,用户将无法使用浏览器,因为这段代码会运行并阻塞线程。

为了解决这个问题,jQuery 对其动画做了一些变通,以确保它们是非阻塞的,以便用户能够在动画运行时与页面进行交互。当您调用animate()——或者任何调用animate()的方法——该动画被添加到一个队列中。这个队列是先进先出(FIFO)队列,这意味着动画被添加到队列中,然后按照添加的顺序运行。一旦一个动画结束,它将触发队列中的下一个动画,如果它存在的话。

这就是为什么div的背景很快变成红色。第一个动画开始运行,然后所有其他的都被添加到队列中,这意味着对css()方法的调用实际上几乎在第一个动画开始时就发生了。

jQuery 通过一系列的setTimeout()调用来执行动画。setTimeout()是一个 JavaScript 方法,在定义的时间间隔后运行代码。当你运行代码使div的不透明度从 1 到 0 时,它实际上会随着时间的推移对不透明度做大量非常小的改变来模拟动画。没有实际的褪色发生。这只是非常迅速地改变了少量的不透明度,给人以动画的错觉。

常见的问题

此队列的一个常见问题是动画的堆积。接下来,您将构建一些代码,在每次单击标题时将动画显示一个div,这样您就可以看到它的实际效果。

用以下内容创建一个index.html文件:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 07, Exercise 02</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <h5>Animate</h5>
    <div id="box">
      box!
    </div>
  </body>
</html>

style.css中添加一些样式:

#box {
  width: 500px;
  height: 500px;
  background: blue;
}

而 JavaScript 在app.js:

$(function() {
  $("h5").on("click", function() {
    $("div").fadeToggle(500);
  });
});

如果你运行这个并点击标题,你会看到div淡出。再点一下就会淡入。现在试着快速点击标题多次。您将看到队列在运行。动画将会建立起来,当您停止单击时,动画仍然会运行,从而产生滞后效果。如果您想避免这种影响,您需要一种在每次运行新动画时清除队列的方法。谢天谢地,jQuery 开发人员也想到了这一点,并提供了stop()方法( http://api.jquery.com/stop/ )。

方法让你清除所有排队的动画。为了避免累积,您可以在添加新动画之前清除当前队列,这意味着您永远不会遇到滞后效应的情况。

首先,尝试将您的app.js文件更改为以下内容:

$(function() {
  $("h5").on("click", function() {
    $("div").stop().fadeToggle(500);
  });
});

尝试多次单击标题。这并不完全符合你的要求。正如 jQuery API 解释的那样:“当在元素上调用.stop()时,当前运行的动画(如果有的话)会立即停止。”

如果你点击它很多次,div会在动画中的随机点停止动画。当不带参数调用stop()时,当前动画立即停止,队列中的下一个动画开始。在清空队列之前,您可能想要完成当前动画。从 jQuery 1.7 开始,stop()方法有三个可选参数:queueclearQueuejumpToEnd,其中最后两个是布尔值,默认为false

第一个参数是一个字符串,表示包含要停止的动画的队列的名称。如果将 true 作为第二个参数传入,jQuery 将清除整个动画队列,清除积压。

如果为第三个参数传递 true,jQuery 将立即跳到动画的结尾。因此,如果当以 true 作为第二个参数调用stop()时,div正处于淡入的中途,div将立即完全淡入。

您需要这两者的结合:

$(function() {
  $("h5").on("click", function() {
    $("div").stop(true, true).fadeToggle(500);
  });
});

在浏览器中运行页面并频繁点击。当您单击时,您会清除队列并结束当前动画,导致没有堆积,从而防止滞后。在任何复杂的滑动功能中,你最有可能使用stop(true, true)

这也是为什么回调在动画中如此重要——它们是确保其中的代码只在动画完成时运行的唯一方法。当您不带参数调用stop()时,回调函数不会被调用。回调函数只在动画结束时调用。如果在第二个参数为真的情况下调用 stop 使动画在调用stop()时立即完成,那么回调将被调用,因为动画已经完成,即使它必须比预期更快完成。

stop()类似的方法是finish()。在 jQuery 1.9 中添加,它完成所有当前正在运行的动画,并删除队列中的所有内容。这两种方法最大的区别在于,stop()设置当前动画的值并删除队列中的所有内容,而 finish()将队列中动画的所有属性设置为结束值。

修理你的手风琴

前面的问题也是你的手风琴所面临的问题。让我们回顾一下在第六章的结尾,你和你的手风琴在哪里。下面是index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 06, Accordion</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div id="accordion">
      <h2>Heading</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
      <h2>Heading 2</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
      <h2>Heading 3</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    </div>
  </body>
</html>

和一些快速的造型:

#accordion {
  width: 500px;
  border: 1px solid black;
}

#accordion h2 {
  padding: 5px;
  margin: 0;
  background: #ddd;
}

#accordion p {
  padding: 0 5px;
}

这是造成滞后问题的 JavaScript 代码:

$(function() {
  var accordion = $("#accordion");
  var headings = $("h2");
  var paragraphs = $("p");
  paragraphs.not(":first").hide();
  accordion.on("click", "h2", function() {
    var t = $(this);
    var tPara = t.next();
    if(!tPara.is(":visible")) {
      tPara.trigger("showParagraph");
    }
  });

  accordion.on("showParagraph", "p", function() {
    paragraphs.slideUp("normal");
    $(this).slideDown("normal");
  });
});

由于标题之间的间隙,实际上很难点击足够的次数来造成任何巨大的延迟,因为你没有动画一个已经可见的段落。这意味着很难点击不同的标题来造成滞后,因为一旦该部分可见,点击相同的标题不会做任何事情。通过改变绑定到您触发的showParagraph事件的函数,修复任何“滞后”效应的可能性:

accordion.on("showParagraph", "p", function() {
  paragraphs.stop(true, true).slideUp("normal");
  $(this).stop(true, true).slideDown("normal");
});

接下来,尝试几种不同的缓动选项。正如前面所解释的,大多数都存在于 jQuery UI 中,所以您需要包含它才能访问它们。然而,仅仅为了它的简化功能而包含整个 jQuery UI 库将是对空间的巨大浪费。令人欣慰的是,该网站让你可以只使用你需要的位( http://jqueryui.com/download/ )进行定制构建,如图 7-2 所示。

img/A310335_2_En_7_Fig2_HTML.jpg)

图 7-2。

The jQuery UI custom builds page

你唯一需要勾选的是效果核心。不要担心勾选任何其他方框或在页面底部填写主题。一旦你点击下载按钮,你会得到一个压缩文件。解压缩 zip 文件后,您会看到如图 7-3 所示的文件夹结构。

img/A310335_2_En_7_Fig3_HTML.jpg)

图 7-3。

The resulting download from the jQuery UI custom build page

您需要的文件(在撰写本文时)称为jquery-ui-1.12.1.custom.min.zip。解压文件并使用“jquery-ui-min.js”。这是定制的,但为你缩小了。将它复制到您的项目文件夹中,并将其重命名为更短的名称,比如简单的jqueryui.js

进入index. html并编辑顶部,添加一个包含jquery-ui-min.js文件的链接。确保在 jQuery 源代码之后这样做,因为 jQuery UI 不出所料地依赖于 jQuery:

<script src="jquery.js"></script>
<script src="jqueryui.js"></script>
<script src="app.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />

要查看现在可用的所有缓解选项,请查看 jQuery UI 文档页面( https://api.jqueryui.com/easings/ )。试试"easeInBack":

accordion.on("showParagraph", "p", function() {
  paragraphs.stop(true, true).slideUp("normal", "easeInBack");
  $(this).stop(true, true).slideDown("normal", "easeInBack");
});

随意试用几款,找到你最喜欢的。最终,我们选定了"easeInCirc":

accordion.on("showParagraph", "p", function() {
  paragraphs.stop(true, true).slideUp(1000, "easeInCirc");
  $(this).stop(true, true).slideDown(1000, "easeInCirc");
});

这段代码有很多重复。这两行看起来非常相似,如果您想更改持续时间或缓动,您必须在两行上进行更改。这从来都不是一个好兆头。正是这种时候,你应该把它抽象成一个效用函数:

var animateAccordion = function(elem, duration, easing) {
  paragraphs.stop(true, true).slideUp(duration, easing);
  $(elem).stop(true, true).slideDown(duration, easing);
}

该函数有三个参数:elem,它指的是您想要向下滑动的元素,以及durationeasing。然后,它像以前一样做同样的动画,向上滑动所有段落,向下滑动活动元素。这确实整理了事件处理程序:

accordion.on("showParagraph", "p", function() {
  animateAccordion(this, 600, "easeInCirc");
});

这可能看起来没什么变化,但在我们看来,让事情变得尽可能简单真的很重要。我们倾向于将这些小函数称为“效用函数”从长远来看,它们节省了大量的时间和打字——而且你几乎肯定会在未来的项目中发现它们的用处。

图像滑块

是时候开始构建图像滑块了。

在深入研究代码之前,您需要考虑您需要它如何工作。在进入 JavaScript 之前,您还必须做一些 CSS 工作。图像列表将被表示为一个无序列表。然后,您可以设置图像的样式,使它们水平布局。你可以根据需要列出足够多的清单来容纳所有人。接下来,这个无序列表位于一个div中,它只有一个图像那么宽,它的溢出属性设置为 hidden。这样,只显示当前图像。然后,操纵无序列表的边距,以动画形式显示图像。

这一切都很简单。只是听起来比实际情况更糟!

从初始设置开始。创建一个新目录,并将 jQuery 源文件放入其中。你还需要index.htmlstyle.cssapp.js文件。

将以下内容添加到您的index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 07 Slider</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div id="slider">
      <ul>
        <li><img src=" https://unsplash.it/300/300/?random " alt="Random Image" /></li>
        <li><img src=" https://unsplash.it/300/300/?random " alt="Random Image" /></li>
        <li><img src=" https://unsplash.it/300/300/?random " alt="Random Image" /></li>
        <li><img src=" https://unsplash.it/300/300/?random " alt="Random Image" /></li>
        <li><img src=" https://unsplash.it/300/300/?random " alt="Random Image" /></li>
      </ul>
    </div>
  </body>
</html>

我们使用相当棒的 Unsplash It 网站( http://unsplash.it )来提供占位符图片。它们在列表项中表示。您需要对这些进行样式化,所以将以下内容添加到您的style.css中:

#slider {
  width: 300px;
  height: 300px;
}

#slider ul {
  list-style: none;
  width: 1500px;
  height: 300px;
  margin: 0;
  padding: 0;
}
#slider li {
  float: left;
  width: 300px;
  height: 300px;
}

如果你查看页面,你应该会看到所有的图片都排成一行离开页面,如图 7-4 所示。

img/A310335_2_En_7_Fig4_HTML.jpg)

图 7-4。

Random images, aligned in a row following the small CSS additions

您可以通过将overflow: hidden;添加到#slider div中来解决这个问题,如下所示。当你这样做的时候,你得到的只是一个随机的图像,就像预期的那样(见图 7-5 )。

img/A310335_2_En_7_Fig5_HTML.jpg)

图 7-5。

Once overflow: hidden; is added, only the first random image is visible.

#slider {
  width: 300px;
  overflow: hidden;
  height: 400px;
}

最后,添加按钮,让用户通过滑块向前和向后导航。将这些添加到无序列表的结束标记之后:

<span class="button back">Back</span>
<span class="button next">Next</span>

然后设计它们的样式:

.button {
  font-family: Arial, sans-serif;
  font-size: 14px;
  display: block;
  padding: 6px;
  border: 1px solid #ccc;
  margin: 10px 0 0 0;
}

.back {
  float: left;
}
.next {
  float: right;
}

也给身体一点填充,只是稍微移动滑块,以便更容易看到:

body {
  padding: 50px;
}

所有这些样式都让您准备好使用 JavaScript。你的图像滑块和按钮看起来应该如图 7-6 所示。

img/A310335_2_En_7_Fig6_HTML.jpg)

图 7-6。

The fully styled image slider and buttons , ready to be implemented

每当你处理一个相当复杂的问题时,考虑列出你需要的所有功能,然后一点一点地实现它。你需要做的是:

  • 当单击 Back 按钮时,将无序列表动画化,使其边距增加 300 像素(一个图像的宽度)。
  • 当单击“下一步”按钮时,动画显示无序列表,将边距减少 300 个像素。
  • 如果在第一个图像,禁用后退按钮。
  • 如果在最后一个图像,禁用下一个按钮。

一旦你完成了所有这些,你将会看到添加更复杂的功能。然而,前面的已经足够让你坚持下去了。所以,让我们开始吧!

首先,存储一些你肯定会用到的变量:

$(function() {
  var sliderWrapper = $("#slider");
  var sliderList = sliderWrapper.children("ul");
  var sliderItems = sliderList.children("li");
  var buttons = sliderWrapper.children(".button");
});

接下来,创建一个函数来激活你的滑块。它需要两个参数:动画的方向,或者是"+"或者是"-",以及动画应该持续的时间。这里有一个例子:

var animateSlider = function(direction, duration) {
  if(direction === "+") {
    sliderList.animate({
      "margin-left" : "+=300px"
    }, duration);
  } else {
    sliderList.animate({
      "margin-left" : "-=300px"
    }, duration);
  }
};

这是一个非常简单的函数,它只是根据方向参数是"+"还是"-"来上下移动 300 个像素。这里当然有一些重构的空间——有很多重复的代码——但是现在的重点是让一个基本的实现工作,然后重新访问代码。

现在您已经有了这个函数,您需要在单击按钮时运行它。这也很简单:

buttons.on("click", function() {
  if($(this).hasClass("back")) {
    animateSlider("+", 1000);
  } else {
    animateSlider("-", 1000);
  };
});

如果你刷新页面,你有一个工作滑块!您只完成了前两个要点,但是恭喜您,您已经实现了一个基本的图像滑块!真的没那么糟。在进入下两个要点之前,做一些重构,特别是在animateSlider()方法中:

var animateSlider = function(direction, duration) {
  if(direction == "+") {
    sliderList.animate({
      "margin-left" : "+=300px"
    }, duration);
  } else {
    sliderList.animate({
      "margin-left" : "-=300px"
    }, duration);
  }
};

这里的重复是可怕的。您只想让对animate的呼叫出现一次。原来有一种简单的方法可以解决这个问题:

var animateSlider = function(direction, duration) {
  sliderList.animate({
    "margin-left" : direction + "=300px"
  }, duration);
};

当您传递方向时,您只需将"=300px"添加到direction变量,这将为您提供"+=300px""-=300px",这正是您所需要的。那好多了。

现在看看按钮上的 click 事件处理程序:

buttons.on("click", function() {
 if($(this).hasClass("back")) {
    animateSlider("+", 1000);
  } else {
    animateSlider("-", 1000);
  };
});

还是那句话,两次调用animateSlider()很乱。这里有一个很好的解决方案,可以将事件处理程序精简为一行代码:

buttons.on("click", function() {
  animateSlider(($(this).hasClass("back") ? "+" : "-"), 1000);
});

这里你使用了一个三元运算符。花点时间更仔细地研究一下:

($(this).hasClass("back") ? "+" : "-")

这只是一个语法上的快捷方式

if($(this).hasClass("back")) { return "+" } else { return "-" }

问号左边的位被评估为真或假。如果为真,则返回问号后的项。如果为 false,则返回冒号后的位。所以在这里,如果按钮有一个"back"的类,就会返回"+";但是如果它没有那个类,它将返回"-"。在这里要小心。虽然你是在往回走,但实际上你是在积极地给页边空白添加动画效果——尽管一开始看起来有些违反直觉。

这让你的整个滑块看起来更整洁。所有这些功能只需 16 行代码!

$(function() {
  var sliderWrapper = $("#slider");
  var sliderList = sliderWrapper.children("ul");
  var sliderItems = sliderList.children("li");
  var buttons = sliderWrapper.children(".button");

  var animateSlider = function(direction, duration) {
    sliderList.animate({
      "margin-left" : direction + "=300px"
    }, duration);
  };

  buttons.on("click", function() {
    animateSlider(($(this).hasClass("back") ? "+" : "-"), 1000);
  });
});

新的问题是,你可以无限地点击任何一个按钮,而页边空白仍然是动态的,在图像应该在的地方留下一个空白。

如果无序列表的 margin 设置为 0,这意味着您在第一个图像,因此 Back 按钮应该被禁用。如果边距设置为–1200 像素,则位于最后一幅图像。这个值就是图像的宽度,乘以你拥有的图像数量。首先,编写一个 helper 函数来告诉你滑块是否在开头:

var isAtStart = function() {
  return parseInt(sliderList.css("margin-left"), 10) === 0;
};

这使用了一个新的叫做parseInt()的 JavaScript 方法,您还没有见过。它接受一个字符串并把它转换成一个整数。对于字符串"300px",它将返回整数 300。它采用的第二个参数是字符串的基数。这是可选的,但是强烈建议您使用它来保证预期的结果。通常情况下,你会使用十进制。如果边距为 0,则您在开始处;如果它不是 0,你就不是在开始,所以你可以简单地返回parseInt(sliderList.css("margin-left"), 10) == 0作为结果。它被评估为真或假。

现在重新编写事件处理程序。这里有一个如何做到这一点的例子:

buttons.on("click", function() {
  var $this = $(this);
  var isBackBtn = $this.hasClass("back");
  if(isBackBtn && isAtStart()) {
    return;
  }
  animateSlider(( isBackBtn ? "+" : "-"), 1000);
});

这存储了$this.hasClass("back")的结果,因为很可能你至少会引用它两次。然后,如果isBackBtn为真并且isAtStart()也为真,您只需return,它将返回并停止事件处理程序的任何进一步执行。这可以确保当您到达开头时,“后退”按钮不起作用。

接下来,当用户在滑块末尾单击后退按钮时,执行相同的操作:

var isAtEnd = function() {
  var imageWidth = sliderItems.first().width();
  var imageCount = sliderItems.length;
  var maxMargin = -1 * (imageWidth * (imageCount-1));
  return parseInt(sliderList.css("margin-left"), 10) === maxMargin;
}

你必须在这里多做一点工作。首先,通过获取单个列表项的宽度来计算图像的宽度。最大边距是项目的宽度乘以图像的数量,再减去 1。这是因为边距为 0 时,显示第一幅图像;所以在–300 像素时,它显示的是第二幅图像,而不是第一幅。你return如果滑块边距确实是最大边距。您的事件处理程序变成

buttons.on("click", function() {
  var $this = $(this);
  var isBackBtn = $this.hasClass("back");
  if(isBackBtn && isAtStart()) {
    return;
  }
  if(!isBackBtn && isAtEnd()) {
    return;
  }
  animateSlider(( isBackBtn ? "+" : "-"), 1000);
});

但是您可以使用 or ( ||)运算符将这些条件合并在一起:

buttons.on("click", function() {
  var $this = $(this);
  var isBackBtn = $this.hasClass("back");
  if( (isBackBtn && isAtStart()) || (!isBackBtn && isAtEnd()) ) { return; }
  animateSlider(( isBackBtn ? "+" : "-"), 1000);
});

请注意,这也将return语句和大括号放在同一行,原因很简单,因为在单独一行中只显示“return”似乎是对空间的愚蠢浪费。您的四个要点已经完成——全部在 31 行 JavaScript 中完成:

$(function() {
  var sliderWrapper = $("#slider");
  var sliderList = sliderWrapper.children("ul");
  var sliderItems = sliderList.children("li");
  var buttons = sliderWrapper.children(".button");

  var animateSlider = function(direction, duration) {
    sliderList.animate({
      "margin-left" : direction + "=300px"
    }, duration);
  };

  var isAtStart = function() {
    return parseInt(sliderList.css("margin-left"), 10) === 0;
  };

  var isAtEnd = function() {
    var imageWidth = sliderItems.first().width();
    var imageCount = sliderItems.length;
    var maxMargin = -1 * (imageWidth * (imageCount-1));
    return parseInt(sliderList.css("margin-left"), 10) === maxMargin;
  }

  buttons.on("click", function() {
    var $this = $(this);
    var isBackBtn = $this.hasClass("back");
    if( (isBackBtn && isAtStart()) || (!isBackBtn && isAtEnd()) ) { return; }
    animateSlider(( isBackBtn ? "+" : "-"), 1000);
  });

});

这是基本的 JavaScript 滑块。

在结束本章之前,还有最后一件事要讲:动画滞后问题。您还希望能够传递一个对animateSlider()函数的回调,因为当您稍后改进这个滑块时(您将把它变成一个插件),它可能会派上用场:

var animateSlider = function(direction, duration, callback) {
  sliderList.stop(true, true).animate({
    "margin-left" : direction + "=300px"
  }, duration, callback);
};

你所需要做的就是调用stop(true, true),这将导致它清空动画队列,并在开始下一个动画之前立即到达当前正在运行的动画的末尾。使用回调很简单:你只需让你的animateSlider()方法接受参数并将其传递给animate()方法。如果不需要使用回调,就不必传入一个。jQuery 会发现回调是未定义的,不会尝试执行它。

如果你连续点击“下一页”按钮几次,你会发现你点击的次数足够多,以至于它可以滚动到末尾。这是为什么?这是因为只有当 margin 正好是–1200 时,isAtEnd()方法才返回 true。但是,如果你在播放动画时点击“下一页”按钮,页边距在–900 到–1200 之间。因此,您实际上想要检查边距是否小于(负值,记住)-900,即the imageWidth * (imageCount - 2):

var isAtEnd = function() {
  var imageWidth = sliderItems.first().width();
  var imageCount = sliderItems.length;
  var maxMargin = -1 * (imageWidth * (imageCount-2));
  return parseInt(sliderList.css("margin-left"), 10) < maxMargin;
}

这解决了这个问题,但是后退按钮有类似的问题。同样,您只需要检查边距是否大于–300 像素,而不是正好为零。

var isAtStart = function() {
  return parseInt(sliderList.css("margin-left"), 10) > -300;
};

现在你有了一个更加健壮的滑块。

摘要

你的滑球还有很多工作要做。在本书的后面,当您将它转换为 jQuery 插件时,您将会看到这一点。作为将它转化为插件的过程的一部分,您将重构和重做它。您还将看到如何让它无限无缝地滚动。

动画是 jQuery 的重要组成部分。这是一个重要的章节:您已经改进了您的手风琴,编写了迄今为止最复杂的 JavaScript,并且了解了 jQuery 的动画是如何在幕后工作的。

八、jQuery Ajax

Ajax 代表异步 JavaScript 和 XML,让我们可以在后台异步地向服务器发送数据,而不会影响用户的体验。

在第七章中,你看到了异步行为的例子。当动画运行时,您能够执行其他代码,比如更改元素的背景颜色,并且用户(在本例中是您)完全能够在动画运行时使用页面。除了动画之外,似乎没有什么不同。用 Ajax 获取数据非常类似。作为用户,在数据被获取并显示在页面上之前,您不会意识到发生了什么。

在这一章中,你将彻底探索 Ajax。虽然 Ajax 代表“异步 JavaScript 和 XML”,但现在获取数据的最常见格式是 JSON,即 JavaScript 对象符号;在开始获取数据之前,您将熟悉这种格式。接下来,您将看到一些示例 JSON,并了解如何使用 JavaScript 来处理它。然后,将向您介绍 jQuery 的ajax()方法。最后,您将使用现实世界中的第三方 API 来获取数据并将其显示在页面上。为此,您需要探索 JSONP,这是一种从第三方网站请求数据的方法。

Ajax 在最近几年有点流行,但是它的真正含义可能会令人困惑。这只是一种异步获取数据的方式。就这样。

数据

Ajax 中的“x”可能代表 XML,但目前几乎每个人都喜欢的格式是 JSON,它代表 JavaScript 对象符号( http://json.org )。

JSON 数据看起来非常类似于常规的 JavaScript 对象。这里有一个例子:

  "name":"Jack Franklin",
  "age":20,
  "location":"London, UK",
  "friends":[
    "Grant",
    "Jamie",
    "Dan",
    "Richard",
    "Alex"
  ]
}

这里有两件重要的事情需要注意。首先,与 JavaScript 不同,JSON 对象中的键必须用双引号括起来。例如,在 JavaScript 中,以下所有内容都有效:

var jack = { "age": 20 };
var jack = { 'age': 20 };
var jack = { age: 20 };

然而,在 JSON 中只有第一行是有效的。

JSON 中的值可以是以下类型:

  • 线
  • 数字
  • 排列
  • 布尔值真/假

数组中的项也可以是这些类型中的任何一种。字符串需要用双引号括起来,但其余的不需要:

{
   "num":2,
   "decimal":2.5,
   "boolean":true,
   "array":[
      1,
      2,
      3,
      true,
      null
   ]
}

与 JavaScript 对象非常相似,键值对后面需要一个逗号,除非它是对象中的最后一对。

所有当前的浏览器都带有用于解析 JSON 的原生 JS 方法。有两种主要方法:

  • JSON.stringify():获取一个 JavaScript 对象并从中产生一个 JSON 字符串。
  • JSON.parse():获取一个 JSON 字符串,并将其解析为一个 JavaScript 对象。

“我可以使用”网站( http://caniuse.com/json )显示了当前所有浏览器都支持 JSON(见图 8-1 )。

img/A310335_2_En_8_Fig1_HTML.jpg)

图 8-1。

Table showing JSON support across multiple browsers

用 JavaScript 解析 JSON

从一个基本的 JSON 字符串开始:

var json = '{ "person" : { "age" : 20, "name" : "Jack" } }';

现在,JavaScript 在这里看到的只是一个字符串。它不知道它其实是 JSON。您可以将它传递给JSON.parse()以将其转换成 JavaScript 对象:

var parsed = JSON.parse(json);
console.log(parsed);

这给了你:

{  person:   { age: 20, name: 'Jack' } }

现在它是一个普通的 JavaScript 对象,您可以访问属性,正如您所期望的那样,使用以下两种符号之一:

console.log(parsed.person);
console.log(parsed.person["age"]);

这给出了以下内容:

{ age: 20, name: 'Jack' }
20

如果 JSON 出错,就会看到一个错误。以下面的无效 JSON 为例:

var invalid = '{ person: "Jack" }';

这是无效的,因为键person应该用引号括起来。尝试在 Google Chrome 中运行JSON.parse(invalid)会得到这个结果(其他浏览器可能会显示稍微不同的错误信息):

SyntaxError: Unexpected token p

这很容易解决,如下所示:

var invalid = '{ "person": "Jack" }';

您可以反过来将对象转换为字符串:

var json = {
  person: {
    age: 20,
    name: "Jack"
  }
}
console.log(JSON.stringify(json));

这将为您提供一个包含以下内容的字符串:

{"person":{"age":20,"name":"Jack"}}

当处理来自 jQuery Ajax 调用的 JSON 响应时,您不必太担心解析 JSON。jQuery 会帮你解决这个问题。

带有 jQuery 的 Ajax

jQuery 附带了jQuery.ajax(),一种复杂而强大的处理 Ajax 请求的方法( http://api.jquery.com/jQuery.ajax/ )。这个方法与您见过的其他方法不同,因为您不是在包含元素的 jQuery 对象上调用它,而是在 jQuery 对象本身上调用它。大多数方法是在元素集上调用的;例如:

$("div").addClass("foo");

这将对 jQuery 对象$("div")中的每个元素调用addClass()方法,该对象是页面上的所有div元素。然而,使用$.ajax()方法,您只需调用

$.ajax(...)

看看如何向一个虚构的 URL 发出请求来获取一些 JSON。稍后,您将使用一个实际的 API,但是现在,请熟悉这个方法。使用$.ajax()方法,您可以传入一个参数,它是选项的对象,或者您可以传入两个参数。第一个是要传入的 URL,第二个是选项的对象。我们更喜欢第一种方法—传入一个包含 URL 属性的对象,例如:

$.ajax({
  "url": "/myurl",
  //more settings here
});

或者

$.ajax("/myurl", { ... });

我们更喜欢第一种方法,因为我们认为将每个选项放在一个对象中,并将每个选项放在自己的行上更清晰。因此,这是我们将在整本书中使用的风格。这也是我们看到大多数其他人使用的方法,这是效仿的另一个好理由。但是,如果您喜欢其他语法,请随意使用。

最重要的参数是 URL,它可以是本地 URL(与脚本在同一个域中)或外部 URL(托管在不同的域中)。如果您想使用外部 URL,还需要做一些工作,所以现在,假设它是一个本地 URL,将返回 JSON 格式的数据。稍后您将获得外部 URL。

该 URL 相对于脚本加载到的页面。这可能因您的站点结构而异,因此确定脚本将访问哪个 URL 的最佳方法是将其设为绝对 URL,也就是说,在开头添加一个“/”,以便 URL 相对于域根。

这些年来,可以设置的属性列表不断增加。完整列表请参考文档( https://api.jquery.com/jQuery.ajax/ )。

这里我们将概述几个重要的属性来说明如何使用 jQuery 从服务器发出请求。

添加 URL 后,您还可以添加类型。这是应该提出的请求类型。缺省值是GET,这是您请求从 URL 获取数据的时候。另一个是POST,就是你要向服务器发送(或者 post)数据的时候。

接下来是dataType,它表示返回给您的数据类型。jQuery 在这里相当聪明,因为它能够相当准确地猜测,但是我们喜欢明确地设置dataType,几乎总是设置为“json”。

为了查看 Ajax 调用的成功或失败,您可以链接done()fail()方法:

$.ajax({
"url" : 'https://jsonplaceholder.typicode.com/posts'
}).done(function(data){
      //if the call is successful
      console.log(data)
}).fail(function(jqXHR, textStatus, errorThrown){
     //if the call is not successful
}).always(function(){
    //runs all the time
});

Note

从 jQuery 1.8 开始,不推荐使用error()success(),这意味着不应该使用它们;相反,请使用以下内容:

done(),替代success()

fail(),替代error()

always(),无论请求是否成功都会运行

再次注意 Ajax 调用是异步的,这一点很重要。当某个东西是异步的时,它在后台运行,并且不会阻止你的代码的其他部分的执行。这也称为非阻塞。异步运行会使它成为非阻塞的,因为它不会阻止下面的代码执行。一些基本的东西,比如对console.log的调用,被阻塞了,因为当它运行时(尽管只是一小段时间),在它完成之前,其他任何东西都不能运行。

当您发出 Ajax 请求时,无论 Ajax 调用是否结束,Ajax 请求代码之后的代码都会立即运行。当你看第七章的动画时,你必须使用回调函数,因为动画下面的代码会立即运行。动画也是异步的,正如您在动画中使用回调一样,您也可以在 Ajax 中这样做。因此,您不能只在函数中返回您的数据,因为当 Ajax 方法被调用时,数据将花费任意长的时间返回给您,并且它是在您的其他代码执行时发生的。因此,您将知道只有在获得数据后才会运行的函数链接起来。这很像您在动画元素中使用的回调,您知道只有在动画完成后才会运行。

把这些都放在一个例子里。像往常一样,获取一个index.html页面,对于本例,它应该只包含以下内容:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 08, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
  </body>
</html>

不需要任何实际的 HTML 元素或样式表。接下来,创建一个名为sample.json的文件。这将包含一些 JSON,您将通过 Ajax 请求获取这些 JSON。将以下内容放入其中:

  "people": [
    { "name": "Jack", "age" : 20 },
    { "name": "Grant", "age": 21 },
    { "name": "Lisa", "age": 21 }
  ]
}

这是一个 JSON 对象,包含一个带有agename属性的人员数组。在您的app.js文件中实现 Ajax 调用。注意下面是如何使用/sample.json中的url的。这意味着 JSON 文件必须位于项目根目录中。如果没有,或者如果你宁愿把它放在子目录中,修改这个。

$(function() {
  $.ajax({
    "url": "/sample.json",
    "type": "get",
    "dataType": "json",
  }).done(function(results){
    console.log(results);
     });
});

设置本地开发服务器

由于 Ajax 的内部特性,您将无法在浏览器中打开index.html文件。您需要在本地服务器上运行代码才能正常工作。如果你在 macOS 上,你可以通过 Python 轻松地运行一个简单的 HTTP 服务器,Python 是默认安装在 macOS 机器上的。只需在项目目录下的终端窗口中运行以下命令:

python-m simple httpserver(python-m 简单 http 服务器)

然后你可以在浏览器中加载http://localhost:8080来查看它。设置本地服务器的另一种跨平台方式是使用 Node.js. Node.js 可以安装在所有平台上。得到它的一个方法是去 https://nodejs.org 。这将为您的平台提供运行时。

安装后,Node.js 包含一个包管理器。节点包管理器(npm)允许您下载 JavaScript 内置的工具,对于本章的目的,它将允许您使用 Node.js 作为本地服务器。

要使当前文件夹成为 web 服务器的根目录,您必须使用 macOS 上的终端或 Windows 上的命令行工具(例如 Git Bash)。在命令行中,导航到要提供服务的文件夹,并安装 http-server 模块。使用命令npm install http-server –g–g标志表示将在全球范围内安装。安装后,您可以将任何文件夹设置为服务器。

如果您尚未找到想要提供的文件夹,请确保导航到该文件夹。然后通过键入http-server启动服务器。这将创建一个服务器,您现在可以打开浏览器并转到 localhost:8080。

一旦你设置好了,在你的浏览器中查看站点并打开开发者工具。你应该会看到类似图 8-2 的东西,它显示了 Chrome 开发者工具,但是所有的开发者工具都应该显示相似的输出。

img/A310335_2_En_8_Fig2_HTML.jpg)

图 8-2。

The object parsed from the JSON that the Ajax call returned

您可以看到 jQuery 将 JSON 转换成了一个对象——因此您不必进行解析阶段。如果你给 Ajax 方法一个错误的 URL,你会看到一个抛出的错误,如图 8-3 所示。

img/A310335_2_En_8_Fig3_HTML.jpg)

图 8-3。

The error thrown if the Ajax URL is incorrect, shown in the Chrome developer tools console

现在看看如果你定义了fail()方法会发生什么(见图 8-4 ):

$(function() {
  $.ajax({
    "url": "/sample2.json",
    "type": "get",
    "dataType": "json"
  }).fail(function(){
      console.log(‘fail’, arguments);
   });
});

您可以通过fail()函数输出传递回来的细节,但我们从未真正发现它有什么大用处。

img/A310335_2_En_8_Fig4_HTML.jpg)

图 8-4。

The output from the fail() function , shown in the Chrome developer tools console

就像你在动画中看到的fadeIn()fadeOut()便利类型的方法一样,Ajax 也有一些。您可以使用getJSON()方法( http://api.jquery.com/jQuery.getJSON/ )将前面的示例修改如下:

$(function() {
  $.getJSON("/sample.json", function(data) {
    console.log(data);
  });
});

这相当于以下内容:

$.ajax({
  "url": "/sample.json",
  "dataType": "json"
}).done(function(){
});

如你所见,它节省了一点工作。您会注意到没有定义错误消息。这是因为getJSON()只支持在 Ajax 请求成功时定义一个函数。您可能认为这是可以接受的,但是这是处理错误的最佳实践,所以您可能会像我们一样,坚持直接使用$.ajax(),这样您就可以定义一个错误处理程序。

幸运的是,在 jQuery 1.5 之后,这一切都变得更容易了,延迟(api.jquery.com/category/deferred-object/),这是以一种更好的方式管理回调的方法。延迟非常强大,但是您只需要了解 Ajax 工作的皮毛。

让我们花点时间来了解一下jqXHR对象。这是一个浏览器本机XMLHttpRequest对象的包装器 Ajax 请求只需 JavaScript 就能完成,让您的生活变得更加轻松。每个 jQuery Ajax 方法——既有像$.getJSON()这样方便的方法,也有主要的$.ajax()方法——都返回这个对象的一个实例。然后你可以做的是在上面添加你的回调方法,这意味着你不必在 Ajax 方法的调用中定义它们。例如,而不是:

$.ajax({
  "url": "/someUrl",
  "success": function() {
    //before deferred objects
    //do something here
  }
});

您可以这样做:

var req = $.ajax({
  "url": "/someUrl"
});

req.done(function() {
  //do something
});

您将$.ajax()(jqXHR对象)的返回值保存到一个变量中,然后可以在该变量上声明回调函数,这样更简洁。

看看传递给这些函数的参数,从done(response, status, jqXHR)开始:

  • response是来自服务器的响应;通常,服务器已经响应了 JSON。
  • status是表示状态的字符串;有了done(),几乎总是"success"
  • jqXHR返回jqXHR对象。

fail(),顺序略有不同;它与您之前使用的错误回调中的参数顺序相同:

.fail(jqXHR, status, errorThrown)

always()传递与.done()fail()相同的参数,这取决于哪一个运行(它们不可能同时运行)。

这样做的真正好处是,您可以设置多个回调:

var req = $.ajax({
  "url": "/someUrl"
});

req.done(function() {
  //do something
});
req.done(function() {
  //do something else
});

req.always(function() {
  //always do this
});

这意味着,如果您在获取数据时有多件事情要做,您可以将它分成多个功能,以使事情更有条理。无论 Ajax 请求是成功还是失败,.always()回调对于执行某些事情非常有用。从这本书的这一点开始,我们将使用这种回调风格,我们鼓励你也这样做。

一个真正的 API: TVmaze

TVmaze ( http://www.tvmaze.com )是一个面向各种电视节目粉丝的网站。这个站点还有一个免费开放的 API,可以返回 JSON 对象格式的结果。你可以在 http://www.tvmaze.com/api 找到文档。

例如,如果您想获得关于电视节目或电影的信息,您可以使用带有shows?q=showName的 URL,其中占位符showName是您正在搜索的节目的名称。这将返回一个 JSON 对象,其中包含 ID、语言和摘要等信息。搜索“重力瀑布”( http://api.tvmaze.com/search/shows?q=Gravity%20Falls )会返回一个类似于图 8-5 所示的物体。

img/A310335_2_En_8_Fig5_HTML.jpg)

图 8-5。

The results of a call to the TV Maze API, returned as a JSON object

有了这些信息,你现在可以提出更具体的要求。可以拿着身份证号点播所有剧集。文档使用:id作为占位符。调用API . TV maze . com/shows/:id/episodes检索剧集列表。代码应该是这样的:

$(function() {
  var req = $.ajax({
    url: " http://api.tvmaze.com/shows/396/episodes "
  });
  req.done(function(data) {
    console.log(data);
  });

});

加载控制台,看看会得到什么(见图 8-6 )。

img/A310335_2_En_8_Fig6_HTML.jpg)

图 8-6。

A partial list of results from the network section of Chrome’s developer tools

可能存在调用远程服务器返回错误的情况。可能是这样的:“XMLHttpRequest 无法加载 http://api.mysite.com 。访问控制允许来源不允许来源http://localhost:8000

请注意,不是所有的浏览器都会解释 Ajax 请求失败的原因,而只会通知您失败了。如果你对请求失败的原因有疑问,在浏览器中加载是个好主意,你知道浏览器会更明确地显示错误,比如 Chrome 的控制台。

这涉及到安全问题。默认情况下,浏览器不允许一个域对另一个域上的 URL 进行 Ajax 调用来获取数据,因为这可能有潜在的危险。Mozilla 开发者网络( https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS )很好地解释了这一点:

When a resource requests resources from different domains, protocols or ports, it sends out HTTP requests from different sources. For example, an HTML page from http://domain-a.com sends a < img > src request to http://domain-b.com/image.jpg. Nowadays, many pages on the web load resources from different fields, such as CSS style sheets, images and scripts. For security reasons, browsers restrict cross-source HTTP requests from within scripts. For example, XMLHttpRequest and Fetch follow the homologous policy. Therefore, web applications using XMLHttpRequest or Fetch can only make HTTP requests to their own domains. In order to improve web applications, developers require browser vendors to allow cross-domain requests.

当然,当涉及到使用第三方 API 时,这是不实际的,因此,存在许多变通办法。一种解决方法是跨源资源共享(CORS ),它允许服务器在其响应中包含一个标头,说明跨源请求是有效的。

另一种解决方案,也是现在常用的一种,是使用 JSONP,它代表 JSON Padded。Johnny Wey ( https://johnnywey.wordpress.com/2012/05/20/jsonp-how-does-it-work/ )的一篇博客文章对此进行了详细的解释,但这篇博客文章特别触及到了其工作原理的本质:

The idea of JSONP is actually very simple: put a script tag into DOM and reference a resource that returns JSON data. Let the server return JSON with "padding" (the "p" part of JSONP), which performs a function to wrap the incoming data.

此时,您可能已经意识到了一些事情:您不能向服务器发出请求来访问 JSON,但是您可以包含外部样式表或脚本。例如,第一章向您展示了如何将来自 Google CDN 的 jQuery 源代码包含在常规的script标签中:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>

如果您可以将所需数据的 URL 放入一个script标记中,并以 JavaScript 脚本的形式获得服务器的响应,从而允许您获取数据,会怎么样?JSONP 就是这样工作的。只要服务器支持它(大多数流行的 API 都会支持),您就可以使用它。它的工作原理是将来自服务器的响应封装在一个函数中。例如,服务器可能会响应

someFunction(data);

data是 JSON 数据。然后将它作为常规 JavaScript 包含在您的页面中,然后您可以定义someFunction来处理数据。这就是 JSONP 的工作方式。令人欣慰的是,jQuery 为您做了所有这些,使它变得更容易;因此,在使用 jQuery 时,您不需要担心这些细节,但是知道事情是如何工作的是很好的。

那么,使用 jQuery 有多容易呢?难以置信。这里有一个例子:

$(function() {
  var req = $.ajax({
    url: "http://api.remote-site.com/show/626625"
  });
  req.done(function(data) {
    console.log(data);
  });

});

将 dataType 属性添加到 ajax 调用中:

$(function() {
  var req = $.ajax({
    url: "http://api.remote-site.com/show/626625",
    dataType: "jsonp"
  });
  req.done(function(data) {
    console.log(data);
  });

});

在继续之前,JSONP 有一个重要的警告值得一提,那就是如果出错,错误回调将不会运行。这是一个不幸的交易,你不得不绕开它。

摘要

多么精彩的一章!

  • 向您介绍了 Ajax 请求的概念并展示了它是如何工作的。
  • 您了解了术语“异步”的含义。
  • 您已经熟悉了 JavaScript 对象符号(JSON)。
  • 您研究了 JSON 和跨来源请求的问题,以及如何使用 JSONP 克服这些问题。
  • 您看到了如何使用 jQuery 及其提供的方法进行 Ajax 调用。
  • 您向外部 API 发出了请求,并获得了数据。

在下一章,我们开始编写我们的第一个 jQuery 插件。

九、编写 jQuery 插件

jQuery 插件是初学者倾向于回避或害怕使用的东西。在人们的脑海中,插件似乎是作为难以置信的复杂事物来使用的,但一旦你了解了它们的工作方式,你会发现它们实际上非常简单,你会发现自己在工作的同时制作多个插件。正如本章将要演示的那样,插件并不像您想象的那么复杂。

在探索了插件的好处以及何时将代码转化为插件之后,您将了解不同类型的插件,构建几个小插件来看看它们是如何工作的,并发现如何使它们对潜在的开发人员来说更具可配置性。在下一章中,你将获取你之前编写的代码,包括你在第五章中开始的 accordion 和第八章中的 Ajax 代码,并将它们转换成功能完整的插件。然后,您将编写一个功能全面、复杂的图像滑块插件。但现在,我们会保持简单。

为什么是插件?

在编写代码时,您会注意到一些典型的模式或迹象,值得将其抽象到插件中。如果您发现自己在想象另一个项目或情况,其中您编写的一些代码可以被重用,这是一个好迹象,表明您应该将它变成一个插件。你的手风琴代码就是一个很好的例子。

如果您发现自己在不同的项目中多次编写非常相似的代码,这是一个很好的迹象,表明您应该花时间来开发一个插件,然后可以轻松地重用它。

如果您发现自己不止一次地编写代码来构建一个简单的手风琴,那么是时候停下来构建一个插件了。最初可能会花一点时间,但之后您就有了一个很好的、自包含的插件,您再也不用编写它了。写一次,受益多次。

到目前为止,我们已经在手风琴的上下文中讨论了插件,但是插件确实可以为您经常做的任何事情而制作。它可以是一个只有三行代码的小插件,只是为了让某个动作变得更加简单,也可以是一个复杂的图像滑块那么大。

您的第一个 jQuery 插件

您的第一个 jQuery 插件将会非常简单。它只是为每个被调用的元素记录下ID属性的值。

在编写插件之前,想象一下另一个开发人员将如何使用您的插件是很有用的。在这种情况下,设想执行以下操作:

$("div").logId();

您应该能够打开您的浏览器控制台,看到$("div")返回的每个元素的日志语句。日志应该只是元素的ID属性,如果元素没有属性,则为空。

了解如何创建它的最佳方式是深入并开始编写代码,一边编写一边解释。因此,创建一个新的目录,可能叫做logid-plug-in或类似的目录,并在其中放一些文件。首先,创建index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 09, Exercise 01</title>
    <script src="jquery.js"></script>
    <script src="logid.jquery.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
    <div id="div1">Hello</div>
    <div id="div2">Hello</div>
    <div id="div3">Hello</div>
    <div id="div4">Hello</div>
    <div id="div5">Hello</div>
    <div id="div6">Hello</div>
    <div id="div7">Hello</div>
  </body>
</html>

将 jQuery 源代码也放在目录中一个名为jquery.js的文件中。最后,再创建两个空白文件。第一个应该叫做logid.jquery.js,它将存放您的插件,第二个叫做app.js,在这里您将编写利用插件的代码。将您的插件文件命名为name.jquery.js是惯例,因此您将坚持使用它。

我们将向您展示插件的实现,然后详细解释它。下面是获得您的logId插件的完整代码,您应该将它放入logid.jquery.js:

$.fn.logId = function() {
  return this.each(function() {
    console.log(this.id);
  });
};

下面是你应该输入app.js来测试它的内容:

$(function() {
  $("div").logId();
});

您的控制台中的结果应该如图 9-1 所示。

img/A310335_2_En_9_Fig1_HTML.jpg)

图 9-1。

The output from using the plug-in (on the Chrome developer console)

退一步想清楚这里到底发生了什么。第一行将您的方法logId添加到 jQuery。这也称为扩展 jQuery:

$.fn.logId = function() {...});

$.fn方法添加一个方法意味着它可以用于 jQuery 对象。记住,jQuery 对象是运行$("div")或类似操作的结果。这一行所做的只是向$.fnlogId添加一个新的属性,它相当于一个函数。

下一个有趣的部分是each()方法:

return this.each(function() {...});

在函数中,this的值是调用函数的 jQuery 对象。所以当你调用$("div").logId()的时候,this的值指的是包含$("div")结果的 jQuery 对象,它将包含页面上所有的div。因此,您希望循环遍历this中的每个元素,这样您就可以在每个元素上运行您的代码。

另一件需要注意的重要事情是,您返回了循环的结果。当循环某个东西时,最初的“东西”总是会被返回。所以当你跑步的时候:

return this.each(function() {...});

在它的结尾,this被返回。这意味着您的插件是可链接的,或者有人可以在您的插件之后调用另一个方法,例如:

$("div").logId().fadeOut();

除非您的插件是专门设计来返回特定值的,否则它应该总是可链接的。人们会认为你的插件是可链接的——如果不是,他们会非常困惑。真的没有借口不使它可链接。

最后,在循环中,this的值指的是正在循环的单个 DOM 元素。注意this不是 jQuery 对象,而只是 DOM 元素。如果你想在上面调用 jQuery 方法,你必须先运行$(this)

在循环中,您需要做的就是获取元素的 ID。您可以执行以下操作:

$(this).attr("id");

但是实际上,不需要 jQuery 就可以很容易地获得 DOM 元素的 ID:

this.id

这实现了同样的事情,并且比 jQuery 对等物更精简,所以没有理由不这样做。因此,您现在有了以下内容:

$.fn.logId = function() {
  return this.each(function() {
    console.log(this.id);
  });
};

恭喜你!您已经编写了第一个 jQuery 插件!

改进

尽管如此,这个插件仍然存在一些问题。首先要解决的是如何定义插件的问题。目前是这样做的:

$.fn.logId = function() {...});

但是,不能保证$变量指的是jQuery。有时使用 jQuery 的人不允许它使用$变量,因为另一个脚本或插件可能正在使用它。当然,这不太可能,但是您永远不应该假设$是 jQuery。通过 jQuery 的noConflict()方法( http://api.jquery.com/jQuery.noConflict/ ),很容易让 jQuery 发布$。这里有一个例子:

jQuery.noConflict();
// now, $ is reset to whatever it was before jQuery was loaded

jQuery("div");

jQuery 默认存在两个全局变量:$jQuery。它们都是相同的,所以如果调用了jQuery.noConflict(),您仍然可以通过jQuery方法使用 jQuery,但是不能通过$

为了安全起见,您应该在插件中使用jQuery变量,而不是$:

jQuery.fn.logId = function() {
  return this.each(function() {
    console.log(this.id);
  });
};

但是输入jQuery而不是$是令人恼火的。谢天谢地,有更好的方法来做事。

一种方法是创建一个函数来定义你的函数。你要做的第一件事就是设置$等于jQuery:

var definePlugin = function() {
  var $ = jQuery;
  $.fn.logId = function() {
    return this.each(function() {
      console.log(this.id);
    });
  };
};

definePlugin();

这是可行的,因为函数中定义的变量只在该函数中可用,所以您可以安全地定义$而不用全局定义它。你可能不喜欢这个解决方案,尽管它很有效。定义然后调用一个函数似乎是一种冗长的做事方式。如果有一种方法可以定义并立即执行一个函数就好了…

立即调用的函数表达式

术语“立即调用的函数表达式”(IIFE)是由多产的 JavaScript 开发人员 Ben Alman 在他的个人博客中创造的,他在一篇关于定义然后立即调用的函数的文章中( http://benalman.com/news/2010/11/immediately-invoked-function-expression/ )。这篇文章非常值得一读,但是非常深入,所以当您对 jQuery 更加熟悉,并且希望了解更多关于 JavaScript 的细节时,您可能会想要阅读它。

生活是一个被立即定义然后执行的功能。尝试在浏览器中加载 JS 控制台,然后运行这行代码:

(function() { console.log("hey"); })();

你将会看到“嘿”这个词正对着你。您只是定义了一个函数——尽管是一个匿名函数(您从未给它起名)——并执行它,所有这些都在同一行中。正如您可能已经猜到的,这里的关键是行尾的一对括号和函数定义周围的一对括号。

函数周围的括号是由于 JavaScript 的解析器是如何工作的。如果你尝试:

function() { console.log("hey"); }

您将得到一个语法错误:

SyntaxError: Unexpected token (

这是谷歌 Chrome 提供的语法错误;每个浏览器的做法略有不同。例如,Firefox 会声明“SyntaxError: function 语句需要一个名称。”这是因为解析器认为您正在定义一个新函数,因此期望在关键字function和左括号之间有一个名字。它认为你在声明一个函数。因此,这不起作用,因为您将得到相同的错误:

function() { console.log("hey"); }()

用括号把它括起来,告诉解析器它是一个表达式而不是声明,这意味着它不需要名字,而是把这一行作为函数表达式来解析;换句话说,它评估函数的内容。第二对空括号只是调用新定义的函数。

那么这和最初的问题有什么关系呢?嗯,生命也可以带参数。在您的控制台中尝试:

(function(x) { console.log(x); })("hey");

这里定义一个函数,它接受一个参数x,并记录它。当你调用它的时候,你可以传入这个值。和这样做没什么区别:

function log(x) {
  console.log(x);
};
log("hey");

现在你知道你可以做到这一点,你应该能够看到它如何适用于最初的问题。您可以定义函数,立即执行它,并更简洁地传入 jQuery:

(function($) {
  // our plugin here
})(jQuery);

在那个函数内,你可以使用$作为jQuery,不管它在函数外是否被定义为jQuery。这样,您的插件变得更加健壮:

(function($) {
  $.fn.logId = function() {
    return this.each(function() {
      console.log(this.id);
    });
  };
})(jQuery);

Note

IIFEs 是本书中使用的 JavaScript 中最复杂的部分之一。如果你在这之后对他们感到舒服,你做得真的很好。如果您需要不止一次地阅读这一部分,请不要担心;生活的方式并不简单。如果你想了解更多信息,最好的资源是本·阿尔曼在 http://benalman.com/news/2010/11/immediately-invoked-function-expression/ 的生活文章。

给用户提供选项

当制作一个插件时,让用户选择它在某个地方如何工作可能是有益的。您可以让用户选择文本颜色,或者如果您的插件为 DOM 元素制作动画,可以让用户选择动画的速度。

现在,您将创建另一个类似于第一个插件的小插件。这将是一个记录到控制台的功能,但记录任何属性。因此,您需要让用户传入他们想要记录的属性。

创建一个新目录,可能是logattribute-plug-in,并使用下面的代码创建一个新的index.html文件。它与您之前制作的插件中的 HTML 相同,只是您的插件的文件名不同。

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 09, Exercise 02</title>
    <script src="jquery.js"></script>
    <script src="logattr.jquery.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
    <div id="div1">Hello</div>
    <div id="div2">Hello</div>
    <div id="div3">Hello</div>
    <div id="div4">Hello</div>
    <div id="div5">Hello</div>
    <div id="div6">Hello</div>
    <div id="div7">Hello</div>
  </body>
</html>

创建一个空白的app.js文件和一个logattr.jquery.js文件,其中应该包含以下代码,这是您的插件的基础:

(function($) {
  $.fn.logAttr = function() {
  };
})(jQuery);

您将再次使用围绕您的插件代码的生命,从现在开始,您将对所有插件都这样做。

首先尝试实现以下内容:

 (function($) {
  $.fn.logAttr = function(attr) {
    return this.each(function() {
      console.log($(this).attr(attr));
    });
  };
})(jQuery);

您只需定义一个将属性作为参数的函数,然后在循环中使用attr()方法将其注销。为了进行测试,将这段代码放到app.js中,然后在浏览器中加载index.html:

$(function() {
  $("div").logAttr("id");
});

如果你检查控制台,你会看到每个div的 ID 被记录。

假设您正在为一个客户构建这个,他带着以下请求回来:

  • 如果元素没有指定属性,他们希望能够定义一个备份值来记录。
  • 他们希望能够在使用控制台和简单地警告变量之间切换,因为他们需要测试的一些浏览器没有控制台。

这都是合理的要求。先整理一下备份值。它作为另一个参数是有意义的,所以添加它。然后,如果属性存在,您可以注销它,如果不存在,您可以注销备份值。让我们用下面的例子:

(function($) {
  $.fn.logAttr = function(attr, backup) {
    return this.each(function() {
      console.log($(this).attr(attr) || backup);
    });
  };
})(jQuery);

这是关键的一行:

console.log($(this).attr(attr) || backup);

它之所以有效,是因为如果元素没有属性,就会返回undefined。然后,语句变成

undefined || backup

undefined被评估为假,那么backup被评估并返回。

为了测试这一点,向其中一个div添加一个rel属性:

<div id="div1" rel="someDiv">Hello</div>

然后将app.js的内容改为

$(function() {
  $("div").logAttr("rel", "N/A");
});

当您在浏览器中运行时,您会看到"someDiv"记录了一次,而"N/A"记录了六次。

接下来,向插件添加另一个选项,如果设置为 true,将使用alert而不是console.log:

(function($) {
  $.fn.logAttr = function(attr, backup, useAlert) {
    return this.each(function() {
      if(useAlert) {
        alert($(this).attr(attr) || backup);
      } else {
        console.log($(this).attr(attr) || backup);
      }
    });
  };
})(jQuery);

调用插件时,只需添加true作为第三个参数:

$(function() {
  $("div").logAttr("rel", "N/A", true);
});

然而,这开始变得混乱。更好的选择是让最终用户传入一个键值对对象,这是您想要的选项。你很快就会这么做。首先,您可以做一些重构:

if(useAlert) {
  alert($(this).attr(attr) || backup);
} else {
  console.log($(this).attr(attr) || backup);
}

这里你重复了两次$(this).attr(attr) || backup。最好将它保存到一个变量中:

var val = $(this).attr(attr) || backup;
if(useAlert) {
  alert(val);
} else {
  console.log(val);
}

但是,这可以用三元运算符进一步缩短:

var val = $(this).attr(attr) || backup;
useAlert ? alert(val) : console.log(val);

这是一个更好的实现。

向插件添加选项

我们不喜欢用户配置插件的方式。在我们看来,以下是凌乱的:

$.fn.logAttr = function(attr, backup, useAlert)

如果用户想将useAlert设置为true,但将backup保留为默认值(即undefined,他们必须像这样调用插件:

$("div").logAttr("rel", undefined, true);

这是令人难以置信的混乱。您应该只让用户在不想使用默认值时指定选项。这就是使用对象存储选项的好处。因此,您可以让用户像这样调用插件:

$("div").logAttr({
  attr: "rel",
  useAlert: true
});

这是你在本章中要做的下一件事,在完成你的logAttr插件并使用你之前写的代码查看 accordion 插件之前。

就实现传入对象的能力而言,有三个步骤:

  • 创建一个包含选项的对象,设置为默认值。
  • 允许用户传入包含所需设置的对象。
  • 用用户选项覆盖您的默认值。

前两步非常简单。首先,更改函数,使其接受一个参数,即 options 对象:

$.fn.logAttr = function(opts) {

然后,在该函数中,定义默认值:

var defaults = {
  attr: "id",
  backup: "N/A",
  useAlert: false
};

下一步是采用默认值并用用户传入的选项覆盖它们。插件经常这样做,因此 jQuery 提供了一个名为$.extend ( http://api.jquery.com/jQuery.extend/ )的实用方法来做这件事。它有几个用例,但是您使用的主要用例是它可以接受两个对象并将它们合并成一个对象。如果在两个对象中都定义了一个属性,则后一个对象优先。以这两个物体为例:

var objectOne = { x: 2, y: 3, z: 4 };
var objectTwo = { x: 4, a: 5 };

然后调用$.extend,传入这些对象:

$.extend(objectOne, objectTwo);

这将使用objectTwo并将其合并到objectOne,修改它以包含合并对象的结果。在这种情况下,objectOne会是这样的:

{ x: 4, y: 3, z: 4, a: 5 }

注意,因为x存在于objectTwo中,所以它被用在了存在于objectOne中的x之上。

通常,您不会想要修改任何一个对象,而是从合并的结果创建一个新的对象。这可以通过将一个空白对象作为第一个参数传递给$.extend来实现。$.extend的返回值始终是合并后的对象。

var objectOne = { x: 2, y: 3, z: 4 };
var objectTwo = { x: 4, a: 5 };

var merged = $.extend({}, objectOne, objectTwo);

这为您留下了一个新对象merged,它包含:

{ x: 4, y: 3, z: 4, a: 5 }

但是,objectOneobjectTwo没有被改动。您在插件中使用的就是这种用法。

在您的插件中实现这一点,您最终会得到以下结果:

(function($) {
  $.fn.logAttr = function(opts) {
    var defaults = {
      attr: "id",
      backup: "N/A",
      useAlert: false
    };
    var options = $.extend({}, defaults, opts);
    return this.each(function() {
      var val = $(this).attr(options.attr) || options.backup;
      options.useAlert ? alert(val) : console.log(val);
    });
  };
})(jQuery);

定义默认值后,使用$.extend将用户传入的对象opts合并到默认值中,并创建一个新对象。$.extend返回这个值,所以你把它存储到options,一个新的变量。

之前在您的选项中,您将用户选项称为attrbackup;现在他们存在于options内部,所以不得不被称为options.attroptions.backup

现在您的插件的用法已经改变,所以进入app.js并更新它:

$(function() {
  $("div").logAttr({
    attr: "rel"
  });
});

使用期权的好处现在应该很明显了。对于您正在设置哪些选项,这要清楚得多,因为您是通过键/值对对象来设置它们的。此外,您只需指定不同于默认设置的选项。

现在你已经创建了一个稍微复杂一点的函数,是时候重温你在第五章写的手风琴代码,并把它变成一个插件了。

手风琴式插件

让我们重温一下你为手风琴写的代码。您可以创建一个新目录或复制以前的目录。以下是app.js的内容:

$(function() {
  var accordion = $("#accordion");
  var headings = $("h2");
  var paragraphs = $("p");

  var animateAccordion = function(elem, duration, easing) {
    paragraphs.stop(true, true).slideUp(duration, easing);
    $(elem).stop(true, true).slideDown(duration, easing);
  }

  paragraphs.not(":first").hide();
  accordion.on("click", "h2", function() {
    var t = $(this);
    var tPara = t.next();
    if(!tPara.is(":visible")) {
      tPara.trigger("showParagraph");
    }
  });

  accordion.on("showParagraph", "p", function() {
    animateAccordion(this, 600, "easeInCirc");
  });
});

这是来自index.html的 HTML:

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 09, Accordion Plugin</title>
    <script src="jquery.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div id="accordion">
      <h2>Heading</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
      <h2>Heading 2</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco

laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
      <h2>Heading 3</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    </div>
  </body>
</html>

style.css中你也有少量的 CSS:

#accordion {
  width: 500px;
  border: 1px solid black;
}

#accordion h2 {
  padding: 5px;
  margin: 0;
  background: #ddd;
}

#accordion p {
  padding: 0 5px;
}

在最初的 accordion 中,您还包含了 jQuery UI 源代码,因此您可以使用不同的缓动类型。这次你不会把它包括进去。只是简单地使用默认动画。

开始把它变成一个插件。插件的一个可能用途是在包含所有内容的div上调用它。然后,您必须向它传递用于标题和内容的元素类型。例如,你可以这样称呼它:

$("#accordion").accordion({
  headings: "h2",
  content: "p"
});

创建一个新文件accordion.jquery.js,并将其留空。如果您愿意,您可以加载旧的 accordion 代码作为参考。更改app.js,让它使用您的插件:

$(function() {
  $("#accordion").accordion({
    headings: "h2",
    content: "p"
  });
});

很明显,这不会马上生效,但是你现在的任务是让它生效。你也应该编辑index.html,在app.js之前添加新的accordion.jquery.js文件:

<script src="jquery.js"></script>
<script src="accordion.jquery.js"></script>
<script src="app.js"></script>

现在你可以开始了。在插件文件(accordion.jquery.js)的顶部,写下初始模板:

(function($) {

  $.fn.accordion = function(opts) {
    var defaults = {
      headings: "h2",
      content: "p"
    };
    var options = $.extend({}, defaults, opts);
  };

})(jQuery);

这里你使用一个生命来确保你可以使用$并且它指向jQuery。创建函数,在设置默认选项之前,它将接受一个参数—选项的对象。你可能需要更多的选择;如果有,你可以边走边添加。然后,创建最终的对象集,将用户的选项与默认值合并。

接下来,您将设置包含您的功能的循环。这在插件被调用的每个元素上循环。请记住,您返回它以便您的插件是可链接的。在循环中,您可以通过this获取当前元素,并因此创建一个$this变量,该变量引用$(this),它是 jQuery 对象中包装的当前元素。如果您正在处理一个有很多变量的项目,其中一些引用 jQuery 对象,而另一些不引用,那么可以考虑在引用的变量前面加上一个$作为自己的视觉提示,这些变量是 jQuery 对象。

return this.each(function() {
  var $this = $(this);
});

然后,您需要存储对标题和内容的引用。在最初的手风琴中,您执行了以下操作:

var headings = $("h2");
var paragraphs = $("p");

但是在这种情况下,选择器是在选项中定义的。您还应该将搜索限制在手风琴范围内:

return this.each(function() {
  var $this = $(this);
  var headings = $this.children(options.headings);
  var paragraphs = $this.children(options.content);
});

如果你看看最初的手风琴,你有一个小的实用功能,animateAccordion(),它为你做动画:

var animateAccordion = function(elem, duration, easing) {
  paragraphs.stop(true, true).slideUp(duration, easing);
  $(elem).stop(true, true).slideDown(duration, easing);
}

您将保留这个函数,但有一点不同。因为您不再允许定制缓动,而是坚持使用默认设置,所以请移除该选项。您的插件循环现在应该如下所示:

return this.each(function() {
  var $this = $(this);
  var headings = $this.children(options.headings);
  var paragraphs = $this.children(options.content);

  var animateAccordion = function(elem, duration) {
    paragraphs.stop(true, true).slideUp(duration);
    $(elem).stop(true, true).slideDown(duration);
  };
});

你的下一段代码做动画。它几乎不需要改变。目前是这样的:

paragraphs.not(":first").hide();
accordion.on("click", "h2", function() {
  var t = $(this);
  var tPara = t.next();
  if(!tPara.is(":visible")) {
    tPara.trigger("showParagraph");
  }
});

  accordion.on("showParagraph", "p", function() {
    animateAccordion(this, 600, "easeInCirc");
  });

只有几件需要修改。首先,你现在引用的不是变量accordion,而是$this。第二,两个调用都是通过"h2""p"引用标题和内容。你需要使用options.headingsoptions.content变量来代替。最后,从animateAccordion()参数中删除第三个参数,因为您不再支持不同的放松方法。

这使得代码看起来像这样:

paragraphs.not(":first").hide();
$this.on("click", options.headings, function() {
  var t = $(this);
  var tPara = t.next();
  if(!tPara.is(":visible")) {
   tPara.trigger("showParagraph");
 }
});

$this.on("showParagraph", options.content, function() {
  animateAccordion(this, 600);
});

你现在完成了。将它放入插件的循环中,您的手风琴就完成了!accordion.jquery.js现在应该是这样的:

(function($) {

  $.fn.accordion = function(opts) {
    var defaults = {
      headings: "h2",
      content: "p"
    };

    var options = $.extend({}, defaults, opts);

    return this.each(function() {
      var $this = $(this);
      var headings = $this.children(options.headings);
      var paragraphs = $this.children(options.content);

      var animateAccordion = function(elem, duration) {
        paragraphs.stop(true, true).slideUp(duration);
        $(elem).stop(true, true).slideDown(duration);
      };

      paragraphs.not(":first").hide();
      $this.on("click", options.headings, function() {
        var t = $(this);
        var tPara = t.next();
        if(!tPara.is(":visible")) {
          tPara.trigger("showParagraph");
        }
      });

      $this.on("showParagraph", options.content, function() {
        animateAccordion(this, 600);
      });
    });
  };

})(jQuery);

如果你在浏览器中加载index.html,你应该会看到它像以前一样工作。祝贺您获得第一个“合适的”jQuery 插件!不过,你可以做些改进。首先是为回调提供支持。

添加回调支持

当你在第七章探索动画时,你发现了回调的美妙和运行一些代码的能力。如果您可以允许任何使用您的插件的人定义一个回调函数,该函数将在动画发生时运行,这意味着用户切换到了一个新的部分,这将是非常棒的。

您可能认为添加回调功能很复杂,但实际上并不复杂。首先要做的是给你的默认值添加一个新的选项,定义默认的回调函数应该是什么。如果用户没有定义一个函数,那么可以推测,一旦动画完成,她不想运行任何东西,所以默认应该是一个什么都不做的函数:

var defaults = {
  headings: "h2",
  content: "p",
  callback: function() {}
};

接下来,您需要编辑您的animateAccordion()函数来接受回调。它应该把它作为一个参数,直接传递给动画。但是,您应该只将它传递给其中一个。如果你要把它同时传递给slideUp()slideDown()函数,你会让它被调用两次。两个动画运行的时间相同,因此它们同时完成。这意味着您只能将其添加到一个动画中:

var animateAccordion = function(elem, duration, callback) {
  paragraphs.stop(true, true).slideUp(duration);
  $(elem).stop(true, true).slideDown(duration, callback);
};

最后,您需要编辑对animateAccordion()的调用以通过回调:

$this.on("showParagraph", options.content, function() {
  animateAccordion(this, 600, options.callback);
});

注意options.callback只是对函数的引用。options.callback()会执行函数,这不是你想要的。引用一个没有括号的函数意味着你得到了一个对该函数的引用,而它并不执行。让我们来测试一下。编辑您的app.js文件,以便您传递一个回调:

$(function() {
  $("#accordion").accordion({
    headings: "h2",
    content: "p",
    callback: function() {
      alert("changed");
    }
  });
});

现在,无论何时任何东西在手风琴里滑落,你都会收到警告。

您要做的最后一个调整是允许用户设置持续时间。对于任何包含动画的插件,让用户改变速度是很重要的。这很容易做到。添加默认值:

var defaults = {
  headings: "h2",
  content: "p",
  callback: function() {},
  duration: 600
};

同样,这只是编辑对animateAccordion()的调用:

$this.on("showParagraph", options.content, function() {
  animateAccordion(this, options.duration, options.callback);
});

至此,您的 accordion 插件就完成了。下面是最终的代码:

(function($) {

  $.fn.accordion = function(opts) {
    var defaults = {
      headings: "h2",
      content: "p",
      callback: function() {},
      duration: 600
    };

    var options = $.extend({}, defaults, opts);

    return this.each(function() {
      var $this = $(this);
      var headings = $this.children(options.headings);
      var paragraphs = $this.children(options.content);

      var animateAccordion = function(elem, duration, callback) {
        paragraphs.stop(true, true).slideUp(duration);
        $(elem).stop(true, true).slideDown(duration, callback);
      };

      paragraphs.not(":first").hide();
      $this.on("click", options.headings, function() {
        var t = $(this);
        var tPara = t.next();
        if(!tPara.is(":visible")) {
          tPara.trigger("showParagraph");
        }
      });

      $this.on("showParagraph", options.content, function() {
        animateAccordion(this, options.duration, options.callback);
      });
    });
  };

})(jQuery);

然后,您可以使用此选项来设置动画,如下所示:

$(function() {
  $("#accordion").accordion({
    duration: 10000
  });
});

摘要

不要低估插件的力量。现在,您有了一个可以在任何项目中使用的简单手风琴。这是真正的好处:插件为您提供了模块化、可移植的代码,您可以根据需要获取和重用这些代码。在下一章中,您将增加复杂性并为 API 的使用编写一个插件。然后,我们将简要讨论如何分发您的插件,包括缩减您的源代码和编写好的文档。

十、更多 jQuery 插件

你以一个非常棒的 accordion 插件结束了上一章,这个插件是基于你在第五章中编写的初始 accordion 代码构建的。在这一章中,你将从第八章中取出你的 TVmaze API 工作,并把它变成一个插件。您将通过创建一个稍微不同类型的插件来实现这一点,这个插件直接存在于 jQuery 对象上。这意味着不像以前那样在集合中调用它:

$("div").somePlugin();

你直接称之为:

$.somePlugin();

在某些情况下,您应该使用第一种风格,而在其他情况下,您应该使用后者。本章解释了后者。本章还包括一些其他内容:

  • 您将进一步探索使您的插件更有用的方法。保持它的通用性,并确保它完成它应该做的工作,使得其他人更容易使用它。您将看到实现这一点的技术。
  • 我们将简要讨论为您的插件编写文档的最佳实践,并展示示例。

TVmaze API 插件

为了提醒您,下面是您编写的用于获取剧集列表的代码,包括我们在第七章中使用的 dataType 属性:

$(function() {
  var req = $.ajax({
    url: " http://api.tvmaze.com/shows/396/episodes ",
    dataType: "jsonp"
  });
  req.done(function(data) {
    console.log(data);
});

是时候把这段代码变成一个 jQuery 插件了。它将直接存在于 jQuery 对象上,因为您将扩展您的插件以拥有两个不同的方法。一个将获得单个节目的数据,另一个将获得该节目的所有剧集并构建一个列表。我们的目标是能够这样称呼它们:

$.tvmaze.getShow();
$.tvmaze.getEpisodes();

因此您的插件将存在于 jQuery 对象上,但是包含不止一个要调用的方法。

首先,为这个项目创建一个新目录。如果您记得以前使用过 npm,这里有一个再次使用它的好机会。如果您没有安装 http-server,请确保已经安装了 Node.js,然后在命令行键入npm install –g http-server。之后,同样使用命令行,键入http-server。这将使您的当前目录成为本地 web 服务器。

你可以通过输入localhost:8080在浏览器中看到你正在做的一切。

首先,您将创建页面的基本结构。为了让它看起来更好,您还将包括 Twitter Bootstrap ( http://getbootstrap.com )。创建空白的app.jstvmaze.jquery.js文件,以及一个index.html文件,我们在这里添加需要使用 Bootstrap 来设计页面样式的部分(参考 http://getbootstrap.com 了解如何从 CDN 使用 Bootstrap 的说明):

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 10, TVMaze Plugin</title>
    <link rel='stylesheet' href='add URL to bootstrap css'>
</head>
  <body>
         <div class="container">
               <h1> TV Maze – TV Show Search </h1>
                  <form>
        <div class="form-group">
           <input type="text" class="form-control" id="showInput" placeholder="Search TV Show" required>
<button tyhpe="submit" class=" btn btn-primary" id-"submitButton">Submit</button>
        </dir>
                 </form>
           </div>
     <div>
            <table class="table">
                    <thead id="tableHead">
                           <tr>
                              <td>Name</td>
                              <td>Network</td>
                             <td>Type</td>
                              <td>Image</td>
                         </tr>
                   </thead>
              <tbody id="tableBody">
                </tbody>
          </table>

         <table class="table">

              <thead>
                   <tr>
                     <td>Airdate</td>
                     <td>Episode Name</td>
                     <td>Episode Number</td>
                     <td>Season</td>
                     <td>Run Time</td>
                     <td>Summary</td>
                  </tr>
            </thead>
             <tbody id="episodeInfo">

             </tbody>
         </table>
    </div>
 </div>
    <script src="jquery.js"></script>
    <script src="tvmaze.jquery.js"></script>
    <script src="app.js"></script>
  </body>
</html>

此时,你的页面应该看起来如图 10-1 所示。

img/A310335_2_En_10_Fig1_HTML.jpg)

图 10-1。

The page with some basic CSS styling

打开tvmaze.jquery.js并添加以下内容。我们添加以下内容来创建 tvmaze 对象,并为其分配方法:

(function($) {

$.tvmaze = {
       getEpisode: function(showId, callback) {
        var req = $.ajax({
        url:'http://api.tvmaze.com/shows/' + showId + '/episodes'
        });
                  req.done(callBack);
    },

    getShow: function(showName, callBack) {
        var req = $.ajax({
                url:'http://api.tvmaze.com/search/shows/?q=' + showName
          });

          req.done(callBack);
   }
};

})(jQuery);

现在插件已经就绪,您可以开始调用 TVmaze 了。随着时间的推移,您可以添加功能来进行其他 API 调用。现在,您可以打开app.js并开始让应用工作。类似于 TVmaze 插件,这将是一个 IIFE(立即调用的函数表达式)。简而言之,它将在创建后立即运行。

(function() {
         $('#submitButton').on ('click', getShowName);

           function getShowName(evt){
                  evt.proventDefault();
                 if($('#showInput')[0].valuie.length > 0){
           let searchValue = $('#showInput')[0].value;
         $().tvmaze.getShow(searchValue, (results) => {
                displayShowResults(results[0].show);
                         });
        }
          }
})();

此时,该函数应该已经执行,jQuery 会向"submitButton"添加一个事件监听器。单击时,调用函数"getShowName()"并评估文本字段的值是否大于 0。如果是,它会将值赋给一个名为searchValue的变量。

在这里,您可以使用您最近创建的插件。使用从文本字段传递的值调用方法getShow()。在这个例子中,您可能会注意到回调函数看起来有点不同。这个例子使用了一个箭头函数。就您的目的而言,它的工作方式与普通的匿名函数完全相同。您的结果返回一个对象数组,然后传递给displayShowResults()函数,您接下来添加它,如下所示:

Note

有关箭头函数与函数表达式有何不同的更多信息,请查看 MDN Web 文档: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

function displayShowResults(results){
    $('#tableBody').html('<td id="episodeName" data-episodeid="' + results.id + '">'
         + result.name + '</td>' + '<td>'
         + checkNetwork(results.network) + '</td>'
         + '<td>' + results.type + '</td>'
         + '<td> <img class="image-border" src=" '
         + results.image.medium '"> </td>');
     $('#episodeInfo tr').remove();

       $('#episodeName').on('click', (event) => {
               getEpisodes(event.target.dataset.episodeid);
        });
}

function checkNetwork(networkName)
  if(networkName != null)  {
       return networkName.name;
    }else{
          return 'Not Listed';
     }
}

在这里,您开始显示您的插件为您进行的 API 调用的结果。jQuery 添加一行结果,显示 ID、名称和图像。还有一个叫做checkNetwork()的函数,只有在 API 返回的结果为 null 的情况下才会包含。在这种情况下,您会得到一个错误。为了避免这种情况,您可以检查是否有空值,如果有,就返回字符串'Not Listed'

下一行删除任何以前的电视节目列表。第一次运行时,不会删除任何内容,但是您很快就会看到这一点变得多么重要。另一个点击事件被添加到 ID episodeName中。

如果您想知道数据属性在做什么,这里是它的用武之地。data 属性是 HTML5 的一部分,允许您添加没有任何可视化表示的额外信息。检索信息时,可以查看dataset属性,调用自己定制的属性episodeid。这样,您现在可以再次调用 TVmaze 插件并获取该电视节目的所有剧集:

function getEpisodes(episodeID){
        $().tvmaze.getEpisodes(episodeID, function(results){
           for(var I = 0; I < results.length; i++){
               $('#episodeInfo').append('<tr> <td>’
                    + results[i].airdate + '</td> <td>'
                    + results[i].name + '</td> <td>'
                    + results[i].number + '</td> <td>'
                    + results[i].season + '</td> <td>'
                    + results[i].runtime + '</td> <td>'
                    + results[i].summary + '<td>'
                    + '</tr>')
           }
        });
}

本例中的最后一个函数调用 TVmaze 插件。当结果返回时,它将遍历这些结果,并在现有的表中添加一个新行,其中包含有关剧集的信息。

如果您在本地 web 服务器(例如,http-server)上运行它,您应该能够调用 API 并获得结果。图 10-2 显示了应该是什么样子的部分截图。

img/A310335_2_En_10_Fig2_HTML.jpg)

图 10-2。

Results from the TVmaze site using the jQuery plugin

文件

我们将非常简要地讨论如何记录您的插件。为您的插件编写文档对其他可能使用它的开发人员来说很重要,但对您来说也是如此。当您在一段时间后重新访问一个插件时,好的文档确实很有帮助。

您需要清楚地记录每个方法。最好提供以下详细信息:

  • 用一两句话描述这个方法的作用。
  • 请注意该方法接受的选项、示例值以及每个选项的默认值。
  • 举例说明插件的用法及其返回的数据。

以下是我如何记录你的getShow()方法的例子:

The Getshow Method

getShow()是一个方法,它接受一个表示电视节目名称的字符串,并向 TVmaze 发出 Ajax 请求,返回该节目的 JSON 数据。

  • showName:这是一个字符串,表示您想要获取其数据的电视节目的 ID;比如:“重力下降”
  • callback:Ajax 请求返回数据时将被调用的函数。

它只需要一个字符串:

下面是一个使用示例,以及收到的响应:

$.tvmaze.getShow({
showName:"Star Wars: Rebels",
  callback: function(data) {
    console.log(data);
  }
});

返回的 JSON 是:

img/A310335_2_En_10_Fig3_HTML.jpg)

这是我如何记录方法的一个例子。我的主要原则是,如果我在考虑是否要记录某个功能,我无论如何都会去做。文档太多总比不够好。

摘要

这是又一个复杂的章节,您在其中探索了组织代码并学习了如何有效地重构。您的插件的最终用户将调用的方法都只有两行长,主要工作在您的工具方法中完成。

接下来,您将通过创建一个图像滑块插件来完成这本书。您将使用到目前为止在本书中学到的所有内容的组合来制作一个包装良好的 jQuery 插件,该插件具有良好的结构,通过选项为用户提供定制,并且可以很容易地重用。

十一、jQuery 图像滑块

您将通过构建著名的 jQuery 插件:image slider 来完成这本书。这将把你到目前为止只孤立研究过的书中的许多部分汇集在一起。您将使用动画来制作图像和事件的动画,让用户点击滑动条,最终得到一个可以投入生产的插件。你还会遇到你还没有研究过的新功能。例如,你可以把你的滑块挂在键盘上,这样用户就可以按向左或向右的箭头来导航。而且,通过允许用户暂停和播放滑块,以及让它每 20 秒自动播放一次动画,您将进一步增加复杂性。尽管你在第七章中制作了一个滑块,你将从头开始这个新的。

行动(或活动、袭击)计划

在着手一个大项目之前,不管它是一个完整的网站还是一个插件,你都应该列出你想要实现的关键特性。这是你在本章要遵循的列表:

  • 允许用户点击屏幕上的前进和后退按钮来浏览图像。
  • 让用户使用左右箭头键浏览图像。
  • 当到达滑块末尾时,使导航按钮循环到开头。
  • 让幻灯片每 20 秒自动播放一次。
  • 如果用户单击“下一步”或“上一步”按钮,重置滑块计时器,这样它就不会在用户单击按钮后自动滚动。

我们开始吧!

项目设置

您已经这样做了很多次,可能已经习惯了,但是您需要用适当的子文件夹创建一个新的项目目录,其中应该包含以下文件(暂时将它们全部留空):

  • index.html
  • css/style.css
  • scripts/app.js
  • scripts/slider.jquery.js

你还应该下载最新版本的 jQuery 并保存到jquery.js。如果你已经安装了 Node.js,那么确保你已经安装了 http-server。

编辑index.html的内容,看起来像下面的代码。这只是加载您的 JavaScript 和 CSS 文件,并设置将成为滑块的 HTML 结构。

<!DOCTYPE html>
<html>
  <head>
    <title>Chapter 11, Image Slider</title>
    <link rel="stylesheet" href="css/style.css">
    <script src="scripts/jquery.js"></script>
    <script src="scripts/slider.jquery.js"></script>
    <script src="scripts/app.js"></script>
  </head>
  <body>
    <div class="slider">
      <ul>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
        <li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
      </ul>
      <a href="#" class="button back">Back</a>
      <a href="#" class="button forward">Forward</a>
    </div>
  </body>
</html>

你将要使用的 CSS 和你在第七章中用于滑块的 CSS 几乎是一样的。它只是让无序列表大到足以水平容纳所有图片,然后让它所在的div的宽度仅够显示一张图片。这里我们使用的图片来自 http://unsplash.com

body {
  padding: 50px;
}

.slider {
  width: 300px;
  overflow: hidden;
  height: 400px;
}

.slider ul {
  list-style: none;
  width: 3000px;
  height: 300px;
  margin: 0;
  padding: 0;
}

.slider li {
  float: left;
  width: 300px;
  height: 300px;
}

.button {
  font-family: Arial, sans-serif;
  font-size: 14px;
  display: block;
  padding: 6px;
  border: 1px solid #ccc;
  margin: 10px 0 0 0;
}

.back {
  float: left;
}

.forward {
  float: right;
}

a:link, a:visited {
  color: blue;
  text-decoration: underline;
}

a:hover {
  text-decoration: none;
}

HTML 和 CSS 就绪后,在命令行运行http-server,你应该会看到类似图 11-1 的东西。

img/A310335_2_En_11_Fig1_HTML.jpg)

图 11-1。

The styled slider, complete with kittens

插件设置

现在在你选择的文本编辑器中打开slider.jquery.js。添加下面的代码,它只对您的插件进行初始设置。它通过用用户传入的设置扩展默认设置来建立settings变量(现在这是空的,但是随着您的继续,您会发现应该将变量转换为选项的地方)。然后它进入循环,该循环遍历调用插件的每个元素,并在其中设置一些初始变量。

function($) {
  $.fn.slider = function(options) {
    var defaults = {};
    var settings = $.extend({}, defaults, options);
    return this.each(function() {
      var $slider = $(this);
      var $sliderList = $slider.children("ul");
      var $sliderItems = $sliderList.children("li");
      var $allButtons = $slider.find(".button");
      var $buttons = {
        forward: $allButtons.filter(".forward"),
        back: $allButtons.filter(".back")
      };
    });
  };
})(jQuery);

请注意,这段代码还将这两个按钮存储在一个对象中。这意味着您可以使用$buttons.forward按钮前进,使用$buttons.back按钮后退。将它们作为$buttons.forward放在一个对象中比每个都有一个变量更好读。要访问它们,您可以使用filter()方法缩小按钮集,只包含您需要的具有特定类的按钮。

接下来,进入您的app.js文件并添加以下内容:

$(function() {
  $(".slider").slider();
});

现在您已经准备好开始了。

制作滑块动画

你在第七章中制作的滑块有一个用于制作滑块动画的实用函数。它有三个参数:方向、持续时间和回调。虽然你不会直接从章节 7 滑块中复制每一段代码,但是你会使用工具方法:

var animateSlider = function(direction, duration, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=300px"
  }, duration, callback);
};

注意一个小的调整:变量现在被称为$sliderList,而不是简单的sliderList

Note

你会注意到,现在我们使用的是混合变量,一些变量以$开头($sliderList),一些没有(settings)。处理大量变量的一种方法是在开头给所有引用 jQuery 对象的变量一个$——以便区分它们。

将该方法添加到设置$buttons对象的行的正下方。现在是时候给按钮添加一个click事件来让滑块工作了。你可以用你在第七章中的同样方法来做这件事。当点击一个按钮时,检查它是否是后退按钮。如果是,以"+"为方向调用animateSlider(),否则以"-"调用。这个方法现在看起来很简单,事实也的确如此。稍后它将需要重构。

$allButtons.on("click", function(event) {
  var isBackButton = $(this).hasClass("back");
  animateSlider((isBackButton ? "+" : "-"), 1000);
  event.preventDefault();
});

滑块在这一点上工作,但有一些大的陷阱。您可以无限地点击按钮,这意味着您可以滚动过去的最后一个图像,并在一个空白页结束。你会记得你在第七章中处理过这个问题,当滑块到达第一张/最后一张图片时,你简单地禁用了后退/前进按钮。这一次,你将会以不同的方式处理它,并使滑块连续循环,这样当你在最后一个图像时点击前进按钮将会把你带回到第一个图像。

无限循环

如果margin-left是 0,你知道滑块在开始,如果它的左边距是-( (numberOfImages -1 ) * widthOfOneImage),你知道滑块在结尾。以下是检测滑块是在开头还是结尾的两个小方法:

var isAtBeginning = function() {
  return parseInt($sliderList.css("margin-left"), 10) === 0;
};

var isAtEnd = function() {
  var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();
  return parseInt($sliderList.css("margin-left"), 10) === -endMargin;
};

记住,$sliderList.css("margin-left")会给你一个字符串——比如“300px ”,所以使用 JavaScript 的parseInt()来从中解析整数 300。parseInt()采用第二个参数,这是使用的基础。在这里,你可以发现你可能在这个滑块中不止一次地解析页边空白,所以把它变成一个新的实用方法。这整理了代码:

var getLeftMargin = function() {
  return parseInt($sliderList.css("margin-left"), 10);
};

var isAtBeginning = function() {
  return getLeftMargin() === 0;
};

var isAtEnd = function() {
  var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();
  return getLeftMargin() === -endMargin;
};

编写一个额外的方法看起来会让你的代码更长,但是它极大地整理了前面的例子;另外,你很可能需要在其他地方使用它。

既然您可以检测滑块是在开头还是结尾,那么您需要对无限循环进行排序。

如果滑块在开头,点击了后退按钮,就需要一直走到结尾。如果滑块在末尾,并且单击了前进按钮,则需要一直跳到开头。前进按钮的行为稍微容易一些,因为将滑块返回到起点只是将左边距设置为 0。

您可以在所单击按钮的事件处理程序中完成此操作。如果单击的按钮不是后退按钮,并且您在末尾,那么您需要循环:

$allButtons.on("click", function(event) {
  var isBackButton = $(this).hasClass("back");
  if(!isBackButton && isAtEnd()) {
    // loop to the beginning
  }
  animateSlider((isBackButton ? "+" : "-"), 1000); event.preventDefault();
});

要循环,您需要将滑块的边距设置为 0。因为您可能需要多次这样做,所以创建另一个实用方法,并将其插入到您定义animateSlider()方法的位置的正下方:

var animateSliderToMargin = function(margin, duration, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left": margin
  }, duration, callback);
};

接下来你要制作动画,因为动画会让用户更清楚地看到发生了什么。下面的代码显示了如何在 click 处理程序中使用新方法。一旦这个改变被实现,你将能够无限循环地向前移动滑块。

$allButtons.on("click", function(event) {
  var isBackButton = $(this).hasClass("back");
  if(!isBackButton && isAtEnd()) {
    animateSliderToMargin(0, 1000);
  } else {
    animateSlider((isBackButton ? "+" : "-"), 1000);
  }
  event.preventDefault();
});

接下来,您将使后退按钮工作。为此,您需要将边距设置为尽可能大的负边距。您在编写isAtEnd()方法时已经计算过了:

var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();

因为您将再次使用它,所以您需要将它移到一个实用方法中,这样就不会重复。然而,在一个工具方法中使用它是多余的。当滑块初始化时,你可以简单地计算这个变量一次,然后在以后引用它。就在您定义变量$buttons的下方,添加以下内容:

var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();

现在更新isAtEnd()方法,简单地使用:

var isAtEnd = function() {
  return getLeftMargin() === -endMargin;
};

你要再做一个改变。与其保持endMargin为正值,并在需要时将其用作-endMargin,不如一开始就简单地将endMargin设为负值要容易得多。将endMargin的变量声明改为如下:

var endMargin = -(($sliderItems.length - 1) * $sliderItems.first().children("img").width());

现在你的isAtEnd()方法更简单了:

var isAtEnd = function() {
  return getLeftMargin() === endMargin;
};

现在,您可以在事件处理程序中使用它来使滑块在后退时无限循环,如下所示:

$allButtons.on("click", function(event) {
  var isBackButton = $(this).hasClass("back");
  if(!isBackButton && isAtEnd()) {
    animateSliderToMargin(0, 1000);
  } else if(isBackButton && isAtBeginning()) {
    animateSliderToMargin(endMargin, 1000);
  } else {
    animateSlider((isBackButton ? "+" : "-"), 1000);
  }
  event.preventDefault();
});

首先,检查前进按钮,确认用户位于滑块上的最后一个图像。如果是这样,动画回到开始。如果没有,检查是否单击了后退按钮,用户是否在滑块的开始位置。如果是,动画回到结尾;否则,你只是像平常一样前后移动,因为用户既不在开始也不在结束。

如果你在浏览器中刷新index.html,你应该可以点击后退按钮,转到列表的最末尾。事实上,你现在应该可以随意点击后退和前进,而不会到达“终点”,因为一旦到达终点,你的滑块就会循环。

赶上

这是一个休息一下,看看我们在哪里的好地方。下面的代码显示了我们的slider.jquery.js文件的全部内容:

(function($) {

  $.fn.slider = function(options) {
    var defaults = {};
    var settings = $.extend({}, defaults, options);

    return this.each(function() {
      // store some initial variables
      var $slider = $(this);
      var $sliderList = $slider.children("ul");
      var $sliderItems = $sliderList.children("li");
      var $allButtons = $slider.find(".button");
      var $buttons = {
        forward: $allButtons.filter(".forward"),
        back: $allButtons.filter(".back")
      };
      var endMargin = -(($sliderItems.length - 1) * $sliderItems.first().children("img").width());

      var animateSlider = function(direction, duration, callback) {
        $sliderList.stop(true, true).animate({
          "margin-left" : direction + "=300px"
        }, duration, callback);
      };

      var animateSliderToMargin = function(margin, duration, callback) {
        $sliderList.stop(true, true).animate({
          "margin-left": margin
        }, duration, callback);
      };

      var getLeftMargin = function() {
        return parseInt($sliderList.css("margin-left"), 10);
      };

      var isAtBeginning = function() {
        return getLeftMargin() === 0;
      };

      var isAtEnd = function() {
        return getLeftMargin() === endMargin;

      };

      $allButtons.on("click", function(event) {
        var isBackButton = $(this).hasClass("back");
        if(!isBackButton && isAtEnd()) {
          animateSliderToMargin(0, 1000);
        } else if(isBackButton && isAtBeginning()) {
          animateSliderToMargin(endMargin, 1000);
        } else {
          animateSlider((isBackButton ? "+" : "-"), 1000);
        }
        event.preventDefault();
      });
    });
  };
})(jQuery);

也看看你可以把一些东西变成用户可以传入的选项。显而易见的是每个动画的持续时间。您在多个地方将持续时间手动编码为 1000,因此将其设置为选项将减少代码的重复性。

编辑defaults变量的声明,为持续时间设置默认值:

var defaults = {
  duration: 1000
};

您需要改变两种方法。首先是animateSlider()法。您将持续时间和回调传递到这个方法中,但是现在您只需要传递方向和回调。将您的animateSlider()方法改为如下所示:

var animateSlider = function(direction, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=300px"
  }, settings.duration, callback);
};

最后,在按钮点击事件处理程序中编辑对animateSlider()的调用,以便只传递方向,而不传递持续时间。你可以通过一个回调,但现在,你不需要,所以不要打扰。

animateSlider((isBackButton ? "+" : "-"));

接下来,更新animateSliderToMargin:

var animateSliderToMargin = function(margin, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left": margin
  }, settings.duration, callback);
};

同样,在事件处理程序中更新对它的调用,使它们不再通过持续时间:

$allButtons.on("click", function(event) {
  var isBackButton = $(this).hasClass("back");
  if(!isBackButton && isAtEnd()) {
    animateSliderToMargin(0);
  } else if(isBackButton && isAtBeginning()) {
    animateSliderToMargin(endMargin);
  } else {
    animateSlider((isBackButton ? "+" : "-"));
  }
  event.preventDefault();
});

在开发插件时,您还应该注意固定值。你所有的价值都需要计算;例如,滑块中每个图像的宽度应该是计算出来的,而不是硬编码的。虽然您已经用这个插件完成了,但是有一个地方我们保留了一个硬编码的值,应该在那里进行计算。你能看到哪里吗?

就在你的animateSlider()法里:

var animateSlider = function(direction, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=300px"
  }, settings.duration, callback);
};

每次以固定的 300 像素制作动画。你应该从一张图片的宽度来计算。您可以在animateSlider()方法中这样做,但是您应该在插件设置好之后,在最顶端进行计算。如果你用animateSlider()方法来做,它会在每次动画运行时重新计算,效率很低。在计算endMargin变量的代码行上方添加以下代码行:

var imageWidth = $sliderItems.first().children("img").width();

然后,您可以整理endMargin变量,使用刚刚计算的imageWidth变量:

var endMargin = -(($sliderItems.length - 1) * imageWidth);

现在在animateSlider()方法中使用这个变量:

var animateSlider = function(direction, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=" + imageWidth
  }, settings.duration, callback);
};

当您传入宽度时,也可以从动画调用中删除“px”——如果您不指定,jQuery 默认为像素。

保持跟踪

在添加键盘支持之前,我们需要实现一个小特性。最好在滑块下方显示一个与滑块中当前图像相关的数字,这样当您第一次加载页面时,它显示 1,当您单击前进时,它显示 2,以此类推。在阅读如何实现之前,先看看你自己能不能做到。你需要

  • 有一个变量来跟踪当前图像。
  • 每次用户后退或前进时更新此变量。
  • 支持无限循环。例如,如果用户在第一张图片上点击后退按钮,数字不应该从 1 到 0,而应该从 1 到最后一张图片,也就是 9。
  • 在 HTML 中添加一个元素来显示数字,并在每次索引改变时更新这个值。

这是我们的解决方案,但尝试自己做。首先,创建新变量:

var totalImages = $sliderItems.length;
var currentIndex = 1;

在 HTML 中,在 Forward 链接下面,添加一个快速的span来显示值:

<span class="index">1</span>

然后,在指定currentIndex的位置下添加另一个变量,以存储对这个新 span 元素的引用:

var $index = $(".index");

index类添加到样式化按钮的同一组 CSS 中,并将文本居中对齐。将这些样式添加到您的style.css文件的底部:

.button, .index {
  font-family: Arial, sans-serif;
  font-size: 14px;
  display: block;
  padding: 6px;
  border: 1px solid #ccc;
  margin: 10px 0 0 0;
}
.index {
  text-align: center;
}

这使得滑块看起来如图 11-2 所示。

img/A310335_2_En_11_Fig2_HTML.jpg)

图 11-2。

The newly styled span tag showing the index

接下来,在您的slider.jquery.js文件中创建一个名为updateIndex()的方法,它接收新的索引。然后,它将这个值存储在currentIndex变量中,并使用您创建的$index变量更新 span 中显示的值。

var updateIndex = function(newIndex) {
  currentIndex = newIndex;
  $index.text(currentIndex);
};

最后,只是在 click 事件处理程序中使用这个方法的问题。下面的代码显示了如何做到这一点。调用它并传入 0 或最后一个数字(如果滑块从末尾循环到开头)。

$allButtons.on("click", function(event) {
  var isBackButton = $(this).hasClass("back");
  if(!isBackButton && isAtEnd()) {
    animateSliderToMargin(0);
    updateIndex(1);
  } else if(isBackButton && isAtBeginning()) {
    animateSliderToMargin(endMargin);
    updateIndex(totalImages);
  } else {
    animateSlider((isBackButton ? "+" : "-"));
  }
  event.preventDefault();
});

接下来,您有显示新的animateSlider()方法的代码。根据动画的前进方向,只需从索引中减去 1 或加上 1。

var animateSlider = function(direction, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=" + imageWidth
  }, settings.duration, callback);
  if(direction == "+") {
    // back button
    updateIndex(currentIndex - 1);
  } else {
    // forward
    updateIndex(currentIndex + 1);
  }
};

当然,您可以使用三元运算符对其进行重构。这个方法现在被很好地精简了:

var animateSlider = function(direction, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=" + imageWidth
  }, settings.duration, callback);

  var increment = (direction === "+" ? -1 : 1);
  updateIndex(currentIndex + increment);
};

再一次,这里是我的整个slider.jquery.js文件,这样您可以很容易地将您的索引实现与我们的进行比较,并确保在您开始添加键盘支持之前,您与我们在同一页上:

(function($) {
  $.fn.slider = function(options) {
    var defaults = {
      duration: 1000
    };
    var settings = $.extend({}, defaults, options);

    return this.each(function() {
      // store some initial variables
      var $slider = $(this);
      var $sliderList = $slider.children("ul");
      var $sliderItems = $sliderList.children("li");
      var $allButtons = $slider.find(".button");
      var $buttons = {
        forward: $allButtons.filter(".forward"),
        back: $allButtons.filter(".back")
      };
      var $index = $(".index");
      var totalImages = $sliderItems.length;

      var imageWidth = $sliderItems.first().children("img").width();
      var endMargin = -(totalImages - 1) * imageWidth);

      var currentIndex = 1;
      var animateSlider = function(direction, callback) {
        $sliderList.stop(true, true).animate({
          "margin-left" : direction + "=" + imageWidth
        }, settings.duration, callback);

        var increment = (direction === "+" ? -1 : 1);
        updateIndex(currentIndex + increment);
      };

      var animateSliderToMargin = function(margin, callback) {
        $sliderList.stop(true, true).animate({
          "margin-left": margin
        }, settings.duration, callback);
      };

      var getLeftMargin = function() {
        return parseInt($sliderList.css("margin-left"), 10);
      };

      var isAtBeginning = function() {

        return getLeftMargin() === 0;
      };

      var isAtEnd = function() {
        return getLeftMargin() === endMargin;
      };

      var updateIndex = function(newIndex) {
        currentIndex = newIndex;
        $index.text(currentIndex);
      };

      $allButtons.on("click", function(event) {
        var isBackButton = $(this).hasClass("back");
        if(!isBackButton && isAtEnd()) {
          animateSliderToMargin(0);
          updateIndex(1);
        } else if(isBackButton && isAtBeginning()) {
          animateSliderToMargin(endMargin);
          updateIndex(totalImages);
        } else {
          animateSlider((isBackButton ? "+" : "-"));
        }
        event.preventDefault();
      });
    });
  };
})(jQuery);

键盘支持

添加键盘支持是我们故意在第 5 和 6 章的事件报道中省略的事情之一,所以我们可以在这里讨论。这实际上比你想象的要简单得多。

其中一个事件是keyup事件。当一个键被按下然后释放时,该事件被激发。你所需要做的就是捕捉那个事件,并在它发生时做些什么。记住,对于每个事件,jQuery 都要通过事件对象;该事件对象的属性之一是keyCode。它对应于触发事件的按键。每个密钥都有一个唯一的数字,它是一个整数。您将使用的两个键是左右箭头键。左箭头的键码是 37,右箭头的键码是 39。如果你想找到其他键的键码,Cambia Research 在 https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes 的博客文章提供了一个全面的列表。

您可以监听keyup事件,并根据键码触发后退或前进按钮上的点击事件。为此,您需要一个元素来绑定keyup事件。它需要是一个始终在焦点上的元素,这样当用户按下箭头时,无论鼠标位于何处,滑块都将工作。

你的第一个想法可能是使用“body”,这是明智的。然而,旧版本的 Internet Explorer 在这方面的支持有点粗糙。在我们对这本书的研究中,我们发现“body”在 IE9 及以下版本上不起作用。实际上最好使用document.documentElement,它是文档对象的一个属性——一个存储文档内容信息的 DOM 对象。documentElement包含对页面上所有内容的引用,因为它返回的元素是根元素——浏览器中的<html>标签。了解了这一点,您可以将一个keyup事件绑定到它,并根据所按下的键触发一次点击:

$(document.documentElement).on("keyup", function(event) {
  if(event.keyCode === 37) {
    //left arrow
    $(".back").trigger("click");
  } else if (event.keyCode === 39) {
    //right arrow
    $(".forward").trigger("click");
  }
});

如果你在浏览器中打开你的滑块,你会发现你现在可以用箭头来控制你的滑块了!就这么简单。

你的下一个挑战是每 20 秒自动激活滑块。为此,您需要能够通过直接调用方法来激活滑块,而不仅仅是通过触发按钮上的 click 事件。这是因为当自动触发动画时,与用户手动点击按钮相比,你需要做不同的事情。因此,您将获取事件处理程序中包含的主要功能,并将其移动到自己的函数中。

调用这个函数triggerSlider(),将事件处理程序的内容移入其中:

var triggerSlider = function(direction, callback) {
  var isBackButton = (direction === "+");
  if(!isBackButton && isAtEnd()) {
    animateSliderToMargin(0, callback);
    updateIndex(1);
  } else if(isBackButton && isAtBeginning()) {
    animateSliderToMargin(endMargin, callback);
    updateIndex(totalImages);
  } else {
    animateSlider(direction, callback);
  }
};

这个函数将接受一个参数,方向,它可以是"+""-"。然后在此基础上设置isBackButton的值。JavaScript 将评估(direction === "+")truefalse,并相应地设置isBackButton的结果。这意味着按钮的 click 事件处理程序要小得多:

$allButtons.on("click", function(event) {
  var isBackButton = $(this).hasClass("back");
  triggerSlider((isBackButton? "+" : "-"));
  event.preventDefault();
});

您需要修改keyup事件处理程序来调用triggerSlider():

$(document.documentElement).on("keyup", function(event) {
  if(event.keyCode === 37) {
    triggerSlider("+");
  } else if (event.keyCode === 39) {
    triggerSlider("-");
  }
});

这给你留下了更好的代码和一种触发动画而不触发点击事件的方法。这是很重要的,因为你会看到下一步,当你研究自动动画你的滑块。

自动动画

为了实现滑块的自动动画,您将使用一个名为setTimeout()的 JavaScript 方法。它有两个参数:一个函数和一个以毫秒为单位的时间。您传入的函数在特定时间后执行;例如:

setTimeout(function() { alert("hey"); }, 1000);

如果你运行这个,你会看到警告弹出,但只是在 1 秒钟后。你可以用它来制作滑块的动画。

为了让你的滑块无限运行,你可以创建一个执行然后调用setTimeout()的函数,把它自己作为第一个参数传递。下面的代码演示了这一点,但是您不应该在浏览器中执行它!你会收到无数的警告。

var alertHey = function() {
  alert("Hey");
  setTimeout(alertHey, 1000);

}
setTimeout(alertHey, 1000);

alertHey函数提醒“嘿”,然后运行setTimeout(), 1 秒钟后调用。一旦你调用了这个函数,它将继续每秒运行一次。

知道了这一点,你就可以很容易地实现你的自动滑动:

var automaticSlide = function() {
  setTimeout(function() {
    triggerSlider("-", function() {
      automaticSlide();
    });
  }, 1000);
};
setTimeout(automaticSlide, 1000);

如果你在浏览器中刷新,你应该看到你的滑块每秒钟都在动。但是有个问题!你可以点击,但这不会停止动画。向后导航特别困难,因为滑块每秒向前移动一次。

在解决这个问题之前,您将添加一个新选项animationDelay,它是自动动画之间的间隔。这里,默认值设置为 5000,比之前的值稍高。如果你想多一点或少一点,请随意调整。

var defaults = {
  duration: 1000,
  animationDelay: 5000
};

然后更新动画代码:

var automaticSlide = function() {
  setTimeout(function() {
    triggerSlider("-", function() {
      automaticSlide();
    });
  }, settings.animationDelay);
};
setTimeout(automaticSlide, settings.animationDelay);

有可能清除正在等待的超时。setTimeout()返回一个 ID,即挂起超时的 ID。然后您可以将它传递给clearTimeout()来取消超时。因此,您需要执行以下操作:

  • 当用户单击按钮时,取消超时。
  • 设置另一个超时时间,但要长得多(可能 30 秒),在这个时间重新启动自动动画。
  • 如果用户同时点击按钮,也取消超时。

首先,插入新的一行,设置初始超时,将其结果存储在变量timer中:

var timer = setTimeout(automaticSlide, settings.animationDelay);

然后,编辑automaticSlide()方法以使用相同的变量:

var automaticSlide = function() {
  timer = setTimeout(function() {
    triggerSlider("-", function() {
      automaticSlide();
    });
  }, settings.animationDelay);
};

现在您在timer变量中有了一个对当前设置的定时器的引用,您可以通过将它传递给clearTimeout()来取消它。为此,创建另一个名为resetTimer()的实用方法。这应该会取消挂起的超时,然后设置一个新的超时,但时间周期要长得多:

var resetTimer = function() {
  if(timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(automaticSlide, 30000);
}

该方法首先检查timer变量是否计算为true,这意味着它包含一个值。如果是这样,就清除超时。然后设置一个新的超时,将结果存储回timer变量。然后,您需要调用此方法两次:第一次,当用户单击按钮时:

$allButtons.on("click", function(event) {
  resetTimer();
  var isBackButton = $(this).hasClass("back");
  triggerSlider((isBackButton? "+" : "-"));
  event.preventDefault();
});

当用户使用箭头键导航时:

$(document.documentElement).on("keyup", function(event) {
  if(event.keyCode === 37) {
    resetTimer();
    triggerSlider("+");
  } else if (event.keyCode === 39) {
    resetTimer();
    triggerSlider("-");
  }
});

有了这些改变,你应该能够使用箭头键或按钮来导航,而不会有自动滑动的阻碍。如果你等 30 秒,自动滑行应该会恢复。

错误修复

对于任何像这样的大型插件,总是会有 bug 出现。我们特意留了一个来演示一个实际的 bug,并告诉你如何解决它。尝试在浏览器中打开滑块,然后快速按下键盘上的左箭头按钮。如果你这样做的次数足够多,你应该会得到如图 11-3 所示的结果。您已经设法跳过了无限循环检测代码,并在 Image–9 上结束。

img/A310335_2_En_11_Fig3_HTML.jpg)

图 11-3。

Definitely a bug that needs fixing!

花点时间想想可能会发生什么。其实一点都不太明显。作为线索,这个 bug 存在于以下两种方法中:

var isAtBeginning = function() {
  return getLeftMargin() === 0;
};

var isAtEnd = function() {
  return getLeftMargin() === endMargin;
};

当你连续点击左箭头时,它会在很短的时间内发出大量的动画。虽然您使用 jQuery 的stop()方法来消除这个问题,但是您可能会在滑块的开始处结束,但是边距不完全是 0。当许多动画快速启动,然后突然停止时,你可能会在两个图像之间的空白处结束。然后,边距可能不是您期望的整数(0、300、600 等)。),反而是略偏。所以你需要不那么具体。如果滑块在开头,左边距将等于或大于 0。同样,如果滑块在末尾,左边距将小于或等于endMargin(小于是因为值是负数,记住)。继续前进,做出改变:

var isAtBeginning = function() {
  return getLeftMargin() >= 0;
};

var isAtEnd = function() {
  return getLeftMargin() <= endMargin;
};

当您现在运行滑块并快速按下箭头键时,您会发现无法跳过开头或结尾。不过,还有一个问题:显示的指数可能会短暂地过高或过低。你有图像 1-9,但是当你太快地点击左箭头时,索引将短暂地到达 0 或 10。你能找出这个错误的来源吗?它在animateSlider()方法中:

var animateSlider = function(direction, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=" + imageWidth
  }, settings.duration, callback);
  var increment = (direction === "+" ? -1 : 1);
  updateIndex(currentIndex + increment);
};

在这里,您在动画开始后立即更新当前索引。相反,你应该在动画结束后更新当前的索引,所以你应该在回调中完成。通过这样做,您可以确保动画完成后,索引会按照您的预期进行更新。这也意味着只有当动画运行到结束时,索引才会更新。您需要重构您的animateSlider()方法,因此在回调中更新索引。一旦这样做了,就可以调用传入到animateSlider()方法中的回调。这显示了您重构的animateSlider()方法:

var animateSlider = function(direction, callback) {
  $sliderList.stop(true, true).animate({
    "margin-left" : direction + "=" + imageWidth
  }, settings.duration, function() {
    var increment = (direction === "+" ? -1 : 1);
    updateIndex(currentIndex + increment);
    if(callback && typeof callback == "function") {
      callback();
    }
  });
};

在回调中,您可以更新索引,然后调用传入的回调。这里要小心,因为你首先需要检查回调是否被设置为某个值,并且它是一个函数。快速条件句会帮你做到这一点。有了这个,你的滑球有了很大的改进,错误也被粉碎了。干得好!

摘要

多么精彩的一章!您从头开始创建了自己的图像滑块,包括自动动画和键盘快捷键。通过适当地清除动画并以数学方式计算所有值,而不是硬编码它们中的任何一个,您已经使它变得健壮和通用。

为了让你欣赏自己的作品,下面是完整的滑块插件:

(function($) {

  $.fn.slider = function(options) {
    var defaults = {
      duration: 1000,
      animationDelay: 5000
    };
    var settings = $.extend({}, defaults, options);

    return this.each(function() {
      // store some initial variables
      var $slider = $(this);
      var $sliderList = $slider.children("ul");
      var $sliderItems = $sliderList.children("li");
      var $allButtons = $slider.find(".button");
      var $buttons = {
        forward: $allButtons.filter(".forward"),
        back: $allButtons.filter(".back")
      };
      var $index = $(".index");
      var imageWidth = $sliderItems.first().children("img").width();
      var endMargin = -(($sliderItems.length - 1) * imageWidth);

      var totalImages = $sliderItems.length;
      var currentIndex = 1;
      var isPaused = false;

      var animateSlider = function(direction, callback) {
        $sliderList.stop(true, true).animate({
          "margin-left" : direction + "=" + imageWidth
        }, settings.duration, function() {
          var increment = (direction === "+" ? -1 : 1);
          updateIndex(currentIndex + increment);
          if(callback && typeof callback == "function") {
            callback();
          }
        });
      };

      var animateSliderToMargin = function(margin, callback) {
        $sliderList.stop(true, true).animate({
          "margin-left": margin
        }, settings.duration, callback);
      };

      var getLeftMargin = function() {
        return parseInt($sliderList.css("margin-left"), 10);
      };

      var isAtBeginning = function() {
        return getLeftMargin() >= 0;
      };

      var isAtEnd = function() {
        return getLeftMargin() <= endMargin;
      };

      var updateIndex = function(newIndex) {
        currentIndex = newIndex;
        $index.text(currentIndex);
      };

      var triggerSlider = function(direction, callback) {
        var isBackButton = (direction === "+");
        if(!isBackButton && isAtEnd()) {
          animateSliderToMargin(0, callback);
          updateIndex(1);
        } else if(isBackButton && isAtBeginning()) {
          animateSliderToMargin(endMargin, callback);
          updateIndex(totalImages);
        } else {
          animateSlider(direction, callback);
        }
      };

      var automaticSlide = function() {
        timer = setTimeout(function() {
          triggerSlider("-", function() {
            automaticSlide();
          });
        }, settings.animationDelay);
      };
      var timer = setTimeout(automaticSlide, settings.animationDelay);
      var resetTimer = function() {
        if(timer) {
          clearTimeout(timer);
        }
        timer = setTimeout(automaticSlide, 30000);
      }

      $allButtons.on("click", function(event) {
        resetTimer();
        var isBackButton = $(this).hasClass("back");
        triggerSlider((isBackButton? "+" : "-"));
        event.preventDefault();
      });

      $(document.documentElement).on("keyup", function(event) {
        if(event.keyCode === 37) {
          resetTimer();
          triggerSlider("+");
        } else if (event.keyCode === 39) {
          resetTimer();
          triggerSlider("-");
        }
      });
    });
  }
})(jQuery);

本章和其他章节的所有代码都可以从 Apress 网站下载。

结论

这就把我们带到了这本书的结尾。我们希望您读完它后,能够自信地利用 JavaScript 和 jQuery 来解决您面临的任何相关问题。除了展示实际用途之外,我们还试图展示重要的概念和方法,希望这些概念和方法能够帮助您解决问题并产生一个健壮的解决方案。如果您想知道接下来该怎么做,我们建议您坚持使用从本书中学到的 jQuery 技能,如果您想更深入地研究 JavaScript,我们建议您阅读一本关于纯 JavaScript 的书。jQuery 只是 JavaScript,JavaScript 越好,jQuery 越好。

posted @ 2024-08-19 17:30  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报