原文:Beginning jQuery
协议:CC BY-NC-SA 4.0
五、事件简介
当您在浏览器中编写 JavaScript 时,您正在编写事件驱动的代码。大部分代码将在发生某些事情时被执行,比如当用户点击一个链接时让内容滑入。尽管您可能没有意识到,但是您已经编写了一些基于事件的代码:
$(function() {
});
如前所述,您已经编写了在文档准备就绪时运行的代码。这是一个要附加代码的事件。它也被称为绑定。出于几个原因,将 JavaScript 保存在单独的文档中并将其绑定到 HTML 文档是个好主意。首先,它使编辑更容易。另一个原因是它防止人们将代码注入 HTML 文档并覆盖您的代码。
到目前为止,您已经绑定了一些在特定事件上运行的代码。在这一章中,你将会看到事件的类型,并且在这一章的最后,使用你的新知识来制作一个手风琴。手风琴是在一个小空间里包含大量信息的好方法。它的工作原理是获取文本段落——每个段落在一个标题下——并且一次只显示文本的一个部分。用户可以通过单击每个标题显示其下方的文本来进行导航。在下一章中,你将会更深入地研究事件(这是一个很大的主题),并改进你在本章中学到的东西。
浏览器中有很多可以绑定的事件。如果您能想到一个事件,那么几乎可以肯定您可以用 jQuery 绑定到该事件。本章介绍以下事件:
click
:点击按钮等元素hover
:通过鼠标与元素交互;在纯 JavaScript 中,称为mouseenter
或mouseleave
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()
方法,你必须使用mouseenter
和mouseleave
事件:
$("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!");
});
对于处理单个输入的事件,您最常使用的两个事件是focus
和blur
,这两个事件正好相反。当一个元素获得焦点时,触发focus
事件。最明显的例子是当用户点击一个输入框或者开始在里面输入的时候。此时,元素获得焦点,并触发focus
事件。当用户继续移动,点击另一个元素或者离开那个元素时,就会触发blur
方法。想想focus
和blur
在工作方式上有点像mouseenter
和mouseleave
。最重要的区别是focus
和blur
可以通过更多方式触发,而不仅仅是通过鼠标。它们也可以在用户浏览表单时通过键盘触发。因此,对于基于活动输入元素触发的事件,永远不要使用mouseenter
或mouseleave
。始终使用focus
和blur
。
$("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
、$t
或self
。你怎么称呼它并不重要——只要确保它是明智的和一致的。没有什么比回到代码中发现自己在不同的地方使用了不同的变量名更糟糕的了!
触发事件
有时您可能想要手动触发一个事件。也许您已经有了一个允许用户填写表单的链接,当它被点击时,您想在表单上触发 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"
时,它将在mouseenter
或mouseleave
上触发(“hover”只是 jQuery 提供的一个方便快捷的方式),因此您所要做的就是在事件处理程序中使用一个简单的语句来确定类型。前面的代码将使div
在鼠标悬停时变成蓝色,当鼠标离开时变成红色。
您可以使用pageX
和pageY
属性来获取事件触发时鼠标相对于文档窗口左上边缘的位置。
$(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 中的元素,但不更新自己。所以现在有两个问题需要解决:
- 如何在一个段落被点击的时候运行一个函数,但是在有很多段落的时候仍然高效地绑定它?
- 如何使插入到 DOM 中的任何新段落在被点击时也能运行代码呢?
正如您可能已经猜到的,答案是事件委托。这意味着不是将事件单独绑定到每个段落,而是将事件绑定到所有段落的父元素,并让它将事件委托给段落。这听起来比实际更复杂。以下是对其工作原理的解释:
click
事件被绑定到所有段落的父元素(保持简单,在这个例子中使用 body 元素)。- 当 body 元素检测到您绑定的事件类型时(本例中为
click
),它会检查点击是否发生在段落上。 - 如果发生了点击,body 元素就会触发。这是授权发生的地方。
这种绑定方式的主要优点是,您必须将一个处理程序绑定到一个元素,并且只需绑定一次。这比你之前做 100 次要好得多。因为这个事件被绑定到一个父元素,而不是段落,这将适用于您插入的任何新段落!
让我们看看你在实践中是如何做到这一点的。像往常一样,建立一个包含 jQuery 和一个空白的app.js
的index.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
orleft
can be animated, butbackground-color
can’t unless jQuery. Use color plug-ins). Unless otherwise specified, the attribute value is considered as the number of pixels. Where applicable, unitsem
and% can be specified. (http://api.jquery.com/animate/
)
插件可用于动画复杂的属性,如颜色,但你通常会动画有一个独特的价值,如宽度或高度。animate()
的基本用法与css()
方法的语法非常相似:
animate({
property: value,
property2: value2
}, 500);
通常情况下,这是方法中使用的形式。它接受一个将属性与值相关联的键-值对对象。
第二个参数是持续时间。与便利方法一样,这可以是fast
、normal
或slow
——分别是 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 只支持两种缓解方法:linear
和swing
。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()
方法有三个可选参数:queue
、clearQueue
和jumpToEnd
,其中最后两个是布尔值,默认为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
,它指的是您想要向下滑动的元素,以及duration
和easing
。然后,它像以前一样做同样的动画,向上滑动所有段落,向下滑动活动元素。这确实整理了事件处理程序:
accordion.on("showParagraph", "p", function() {
animateAccordion(this, 600, "easeInCirc");
});
这可能看起来没什么变化,但在我们看来,让事情变得尽可能简单真的很重要。我们倾向于将这些小函数称为“效用函数”从长远来看,它们节省了大量的时间和打字——而且你几乎肯定会在未来的项目中发现它们的用处。
图像滑块
是时候开始构建图像滑块了。
在深入研究代码之前,您需要考虑您需要它如何工作。在进入 JavaScript 之前,您还必须做一些 CSS 工作。图像列表将被表示为一个无序列表。然后,您可以设置图像的样式,使它们水平布局。你可以根据需要列出足够多的清单来容纳所有人。接下来,这个无序列表位于一个div
中,它只有一个图像那么宽,它的溢出属性设置为 hidden。这样,只显示当前图像。然后,操纵无序列表的边距,以动画形式显示图像。
这一切都很简单。只是听起来比实际情况更糟!
从初始设置开始。创建一个新目录,并将 jQuery 源文件放入其中。你还需要index.html
、style.css
和app.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 对象。
“我可以使用”网站( https://www.qianduange.cn/upload/article/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 对象,包含一个带有age
和name
属性的人员数组。在您的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、语言和摘要等信息。搜索“重力瀑布”( https://www.qianduange.cn/upload/article/shows)
图 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 https://www.qianduange.cn/upload/article/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 插件。