3.5 移除事件处理程序

有时候,我们需要停用以前注册的事件处理程序。可能是因为页面的状态发生了变化,导致相应的操作不再有必要。处理这种情形的一种典型做法,就是在事件处理程序中使用条件语句。但是,如果能够完全移除处理程序绑定显然更有效率。
假设我们希望折叠样式转换器在页面没有使用正常样式的情况下保持扩展状态, 即当Narrow ColumnLarge Print按钮被选中时,单击样式转换器的背景区域不应该引发任何操作。为此,可以在单击非默认样式转换按钮时,调用.off()方法移除折叠处理程序,如代码清单3-18所示。

1
2
3
4
$('#switcher-narrow, #switcher-large').click(function () {
// 移除DOM上的click程序,即折叠处理程序
$('#switcher').off('click');
});

现在,如果单击Narrow Column按钮,样式转换器(<div>)上的单击处理程序就会被移除。然后,再单击背景区域将不会导致它折叠起来。但是,按钮本身的作用却失效了!由于为使用事件委托而重写了按钮处理代码,因此按钮本身也带有样式转换器(<div>)的单击事件处理程序。换句话说,在调用$('#switcher').off('click')时,会导致按钮上绑定的两个事件处理程序都被移除。

3.5.1 为事件处理程序添加命名空间

显然,应该让对.off()的调用更有针对性,以避免把注册的两个单击处理程序全都移除。达成目标的一种方式是使用事件命名空间,即在绑定事件时引入附加信息,以便将来识别特定的处理程序。要使用命名空间,需要退一步使用绑定事件处理程序的非简写方法,即.on()方法本身。
我们为.on()方法传递的第一个参数,应该是想要截获的事件的名称。不过,在此可以使用一种特殊的语法形式,即对事件加以细分,参见代码清单3-19。

1
2
3
4
5
6
7
8
9
10
$(document).ready(function() { 
$('#switcher').on('click.collapse', function(event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
});
$('#switcher-narrow, #switcher-large').click(function() {
$('#switcher').off('click.collapse');
});
});

对于事件处理系统而言,后缀.collapse是不可见的。换句话说,这里仍然会像编写.on('click')一样,让注册的函数响应单击事件。但是,通过附加的命名空间信息,则可以解除对这个特定处理程序的绑定,同时不影响为按钮注册的其他单击处理程序。

3.5.2 重新绑定事件

现在单击Narrow ColumnLarge Print按钮,会导致样式转换器的折叠功能失效。可是,我们希望该功能在单击Default按钮时恢复。为此,应该在Default按钮被单击时,重新绑定事件处理程序。
首先,应该为事件处理程序起个名字,以便多次使用,参见代码清单3-20。

1
2
3
4
5
6
7
8
$(document).ready(function() { 
var toggleSwitcher = function(event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
};
$('#switcher').on('click.collapse', toggleSwitcher);
});

我们注意到,这里使用了另一种定义函数的语法,即没有使用函数声明(前置function关键字) ,而是将一个匿名函数表达式指定给了一个局部变量。除了两点微妙的差异(但在这里并不存在)之外,无论使用哪种语法,它们的功能都是等价的。这里使用函数表达式只是为了从形式上让事件处理程序与其他函数定义显得类似。
而且,我们知道传递给.on()的第二个参数是一个函数引用。在此需要强调一点,使用命名函数时,必须省略函数名称后面的圆括号。圆括号会导致函数被调用,而非被引用
在函数有了可以引用的名字之后,将来就可以再次绑定而无需重新定义它了,如代码清单3-21所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//未完成的代码 
$(document).ready(function() {
var toggleSwitcher = function(event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
};
$('#switcher').on('click.collapse', toggleSwitcher);
$('#switcher-narrow, #switcher-large').click(function() {
$('#switcher').off('click.collapse');
});
$('#switcher-default').click(function() {
$('#switcher')
.on('click.collapse', toggleSwitcher);
});
});

这样,切换样式转换器的行为当文档加载后会被绑定。当单击Narrow ColumnLarge Print按钮时会解除绑定,而当此后再单击Default按钮时,又会恢复绑定。
使用命名函数还有另外一个好处,即不必再使用事件命名空间。因为.off()可以将这个命名函数作为第二个参数,结果只会解除对特定处理程序的绑定。但这样就会遇到另一个问题,当在jQuery中把处理程序绑定到事件时,之前绑定的处理程序仍然有效。在这个例子中,每次点击Default,就会有一个toggleSwitcher的副本被绑定到样式转换器。换句话说,在用户单击NarrowLarge Print之前(这样就可以一次性地解除对toggleSwitcher的绑定),每多单击一次都会多调用一次这个函数。
在绑定toggleSwitcher偶数次的情况下,单击样式转换器(不是按钮),好像一切都没有发生变化。事实上,这是因为切换了hidden类偶数次,结果状态与开始的时候相同。为了解决这个问题,可以在用户单击任意按钮时解除绑定,并在确定单击按钮的ID是switcher-default的情况下再重新绑定,参见代码清单3-22。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$(document).ready(function () {
// 定义命名函数
var toggleSwitcher = function (event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
};
// 一上来就注册点击事件处理程序toggleSwitcher
$('#switcher').on('click', toggleSwitcher);

$('#switcher button').click(function () {
// 移除点击事件处理程序toggleSwitcher
$('#switcher').off('click', toggleSwitcher);
// 该按钮时默认按钮
if (this.id == 'switcher-default') {
// 注册点击事件处理程序toggleSwitcher
// 同一个DOM上可以用多个点击事件处理程序,
$('#switcher').on('click', toggleSwitcher);
}

});
});

对于只需触发一次,随后要立即解除绑定的情况也有一种简写方法——.one(),这个简写方法的用法如下:

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

这样会使切换操作只发生一次,之后就再也不会发生。

总结

off方法

$(选择符).off("方法名称")这个方法可以移除事件处理程序

移除事件上的所有事件处理方法

$('#switcher').off('click');可以移除id为switcherDOM元素上click这个事件注册的所有处理方法。

移除事件命名空间上的所有事假处理方法

为了更精确的设置事件处理方法,可以给事件指定命名空间,在off方法上传入事件命名空间,可以移除该事件命名空间上的所有方法,例如: $('#switcher').off('click.collapse'); 这个方法可以移除事件命名空间click.collapse上所有的事件处理方法.

命名函数

所谓命名函数,将一个匿名函数表达式指定给了一个局部变量,这个局部变量就是一个命名函数.引用命名函数时不要带括号,带括号会使得函数被调用,而不是被引用.

移除命名的事件处理方法

可以把事件处理方法赋值给一个变量,也就是给事件处理方法命名,这样的话,off方法可以直接移除事件上的该方法,而不需要指定命名空间,例如:$('#switcher').off('click', toggleSwitcher);

one方法

one方法可以触发一次,触发后立即解除.

3.4 通过事件对象改变事件的旅程

我们在前面已经举例说明事件冒泡可能会导致问题的一种情形。为了展示一种.hover()也无能为力的情况,需要改变前面实现的折叠行为。
假设我们希望增大触发样式转换器折叠或扩展的可单击区域。一种方案就是将事件处理程序从标签移至包含它的<div>元素。在代码清单3-9中,我们给#switcher h3添加了一个click处理程序,在这里我们要尝试给#switcher添加这个处理程序,如代码清单3-11所示。

1
2
3
4
5
$(document).ready(function() {
$('#switcher').click(function(event) {
$('#switcher button').toggleClass('hidden');
});
});

这种改变会使样式转换器的整个区域都可以通过单击切换其可见性。但同时也造成了一个问题,即单击按钮会在修改内容区的样式之后折叠样式转换器。导致这个问题的原因就是事件冒泡,即事件首先被按钮处理,然后又沿着DOM树向上传递,直至到达<div id="switcher">激活事件处理程序并隐藏按钮。
要解决这个问题,必须访问事件对象事件对象是一种DOM结构,它会在元素获得处理事件的机会时传递给被调用的事件处理程序。这个对象中包含着与事件有关的信息(例如事件发生时的鼠标指针位置),也提供了可以用来影响事件在DOM中传递进程的一些方法。
为了在处理程序中使用事件对象,需要为函数添加一个参数:

1
2
3
4
5
$(document).ready(function() { 
$('#switcher').click(function(event) {
$('#switcher button').toggleClass('hidden');
});
});

注意,这里把事件对象命名为event,这主要是为了让大家一看就知道它是什么对象,不是必须这样命名的。就算你把它命名为flapjacks(煎饼),也没有任何问题。

3.4.1 事件目标

现在,事件处理程序中的变量event保存着事件对象。而**event.target属性保存着发生事件的目标元素。这个属性是DOM API中规定的,但是没有在某些旧版本的浏览器中实现。jQuery对这个事件对象进行了必要的扩展,从而在任何浏览器中都能够使用这个属性。通过event.target,可以确定DOM中首先接收到事件的元素**(即实际被单击的元素)。而且,我们知道**this引用的是处理事件的DOM元素**,所以可以编写出代码清单3-12。

1
2
3
4
5
6
7
8
9
$(document).ready(function () {
$('#switcher').click(function (event) {
// 只处理实际被点击的DOM元素
// this表示DOM元素,包括实际被点击的DOM元素,及其祖先元素(id为switcher的为最顶层的祖先).
if (event.target == this) {
$('#switcher button').toggleClass('hidden');
}
});
});

此时的代码确保了被单击的元素是<div id="switcher">,而不是其他后代元素。现在,单击按钮不会再折叠样式转换器,而单击转换器背景区则会触发折叠操作。但是,单击标签(<h3>)同样什么也不会发生,因为它也是一个后代元素。实际上,我们可以不把检查代码放在这里,而是通过修改按钮的行为来达到目标。

3.4.2 停止事件传播

事件对象还提供了一个.stopPropagation()方法,该方法可以完全阻止事件冒泡。与.target类似,这个方法也是一种基本的DOM特性,但在IE8及更早版本中则无法安全地使用。不过,只要我们通过jQuery来注册所有的事件处理程序,就可以放心地使用这个方法。
下面,我们会删除刚才添加的检查语句event.target == this,并在按钮的单击处理程序中添加一些代码,参见代码清单3-13。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$(document).ready(function () {
// 鼠标进出时调用
$('#switcher h3').hover(function () {
$(this).addClass('hover');
}, function () {
$(this).removeClass('hover');
});
});

$(document).ready(function () {
// 背景的事件处理程序
$('#switcher').click(function (event) {
$('#switcher button').toggleClass('hidden');
});
});

$(document).ready(function () {
$('#switcher-default').addClass('selected');

// 按钮的点击事件处理程序
$('#switcher button').click(function (event) {
// 动态生成class属性
var bodyClass = this.id.split('-')[1];
$('body').removeClass().addClass(bodyClass);

$('#switcher button').removeClass('selected');
$(this).addClass('selected');
// 停止事件传播,该事件不会传播到祖先元素
event.stopPropagation();
});
});

同以前一样,需要为用作单击处理程序的函数添加一个参数,以便访问事件对象。然后,通过调用event.stopPropagation()就可以避免其他所有DOM元素响应这个事件。这样一来,单击按钮的事件会被按钮处理,而且只会被按钮处理。单击样式转换器的其他地方则可以折叠和扩展整个区域。

3.4.3 阻止默认操作

如果我们把单击事件处理程序注册到锚元素(<a>),而不是外层的<div>上,那么就要面对另外一个问题:当用户单击链接时,浏览器会加载一个新页面。这种行为与我们讨论的事件处理程序不是同一个概念,它是单击锚元素的默认操作。类似地,当用户在编辑完表单后按下回车键时,会触发表单的submit事件,在此事件发生后,表单提交才会真正发生。
即便在事件对象上调用event.stopPropagation()方法也不能禁止这种默认操作,因为默认操作不是在正常的事件传播流中发生的。在这种情况下,event.preventDefault()方法则可以在触发默认操作之前终止事件。

在事件的环境中完成了某些验证之后,通常会用到event.preventDefault()。例如,在表单提交期间,我们会对用户是否填写了必填字段进行检查,如果用户没有填写相应字段,那么就需要阻止默认操作。

事件传播和默认操作是相互独立的两套机制,在二者任何一方发生时,都可以终止另一方。如果想要同时停止事件传播和默认操作,可以在事件处理程序中返回false,这是对在事件对象上同时调用.stopPropagation().preventDefault()的一种简写方式。

3.4.4 事件委托

事件冒泡并不总是带来问题,也可以利用它为我们带来好处。事件委托就是利用冒泡的一项高级技术。通过事件委托,可以借助一个元素上的事件处理程序完成很多工作。
在我前面的例子中,只有3个<div class="button">元素注册了单击处理程序。假如我们想为更多元素注册处理程序怎么办?这种情况比我们想象的更常见。例如,有一个显示信息的大型表格,每一行都有一项需要注册单击处理程序。虽然不难通过隐式迭代来指定所有单击处理程序,但性能可能会很成问题,因为循环是由jQuery在内部完成的,而且要维护所有处理程序也需要占用很多内存。
为解决这个问题,可以只在DOM中的一个祖先元素上指定一个单击处理程序。由于事件会冒泡,未遭拦截的单击事件最终会到达这个祖先元素,而我们可以在此时再作出相应处理
下面我们就以样式转换器为例(尽管其中的按钮数量还不至于使用这种方法),说明如何用这种技术。从代码清单3-12中可以看到,当发生单击事件时,可以使用event.target属性查鼠标指针下方是什么元素。下面是代码清单3-14。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$(document).ready(function () {
$('#switcher h3').hover(function () {
$(this).addClass('hover');
}, function () {
$(this).removeClass('hover');
});
});

$(document).ready(function () {
$('#switcher').click(function (event) {
// 隐藏所有的按钮
$('#switcher button').toggleClass('hidden');
});
});

$(document).ready(function () {
$('#switcher-default').addClass('selected');

$('#switcher').click(function (event) {
// 如果触发这个事件的DOM元素时按钮的话
if ($(event.target).is('button')) {
// 根据该DOM元素(event.target)的id生成class属性值
var bodyClass = event.target.id.split('-')[1];
// 移除body元素上的所有class,然后再添加生成的class
$('body').removeClass().addClass(bodyClass);
// 所有按钮都不选中
$('#switcher button').removeClass('selected');
// 选中当前的按钮
$(event.target).addClass('selected');
// 阻止事件冒泡
event.stopPropagation();
}
});
});

这里使用了一个新方法,即.is()方法。这个方法接收一个选择符表达式(第2章介绍过) ,然后用选择符来测试当前的jQuery对象。如果集合中至少有一个元素与选择符匹配, .is()方法返回true。在这个例子中,$(event.target).is('button')测试被单击的元素是否包含button标签。如果是,则继续执行以前编写的那些代码——但有一个明显的不同,即此时的关键字this引用的是<div id="switcher">。换句话说,如果现在需要访问被单击的按钮,每次都必须通过event.target来引用。

is()与`.hasClass()

要测试元素是否包含某个类,也可以使用另一个简写方法.hasClass(),不过,.is()方法则更灵活一些,它可以测试任何选择符表达式。

然而,以上代码还有一个不期而至的连带效果。当按钮被单击时,转换器会折叠起来,就像没有使用.stopPropagation()之前看到的效果一样。用于切换转换器可见性的处理程序,现在被绑定到了按钮上面。因此,阻止事件冒泡并不会影响切换发生。要解决这个问题,可以去掉对.stopPropagation()的调用,然后添加另一个.is()测试。同样,随着把整个转换器<div>变得可以单击,还应该在用户鼠标悬停时切换hover类,如代码清单3-15所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$(document).ready(function () {
// 鼠标悬停事件
$('#switcher').hover(function () {
$(this).addClass('hover');
}, function () {
$(this).removeClass('hover');
});
});

$(document).ready(function () {
$('#switcher').click(function (event) {
// 如果触发事件的DOM元素不是按钮的话
if (!$(event.target).is('button')) {
// 隐藏/显示这些按钮
$('#switcher button').toggleClass('hidden');
}
});
});

$(document).ready(function () {
$('#switcher-default').addClass('selected');

$('#switcher').click(function (event) {
// 如果触发点击事件的DOM元素是按钮的话
if ($(event.target).is('button')) {
var bodyClass = event.target.id.split('-')[1];

$('body').removeClass().addClass(bodyClass);

$('#switcher button').removeClass('selected');
$(event.target).addClass('selected');
}
});
});

虽然这个例子的代码显得稍微复杂了一点,但随着带有事件处理程序的元素数量增多,使用事件委托终究还是正确的技术。此外,通过组合两个click事件处理程序并使用基于.is()测试的if-else语句,可以减少重复的代码,参见代码清单3-16。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$(document).ready(function() {
$('#switcher').hover(function() {
$(this).addClass('hover');
}, function() {
$(this).removeClass('hover');
});
});

$(document).ready(function() {
$('#switcher-default').addClass('selected');

$('#switcher').on('click', 'button', function() {
var bodyClass = event.target.id.split('-')[1];

$('body').removeClass().addClass(bodyClass);

$('#switcher button').removeClass('selected');
$(this).addClass('selected');
});
});

以上代码仍然有进一步优化的余地,但目前这种情况已经是可以接受的了。不过,为了更深入地理解jQuery的事件处理,我们还要返回代码清单3-16,继续在那个版本上修改。

3.4.5 使用内置的事件委托功能

由于事件委托可以解决很多问题,所以jQuery专门提供了一组方法来实现事件委托。前面讨论过的.on()方法可以接受相应参数实现事件委托,如代码清单3-17所示:

1
2
3
4
5
6
$('#switcher').on('click', 'button', function() { 
var bodyClass = event.target.id.split('-')[1];
$('body').removeClass().addClass(bodyClass);
$('#switcher button').removeClass('selected');
$(this).addClass('selected');
});

如果给.on()方法传入的第二个参数是一个选择符表达式,jQuery会把click事件处理程序绑定到#switcher对象,同时比较event.target和选择符表达式(这里的'button')。

  • 如果event.target和选择符表达式匹配,jQuery会把this关键字映射到匹配的元素,
  • 如果event.target和选择符表达式不匹配,则不会执行事件处理程序。

3.3 事件传播

在说明基于通常不可单击的页面元素处理单击事件的能力时,我们构思的界面中已经给出了一些提示——样式表切换器标签(即<h3>元素)实际上都是活动的,随时等待用户操作。为了改进界面,我们可以为按钮添加一种翻转状态,以便清楚地表明它们能与鼠标进行某种方式的交互:

1
2
3
4
.hover { 
cursor: pointer;
background-color: #afa;
}

CSS规范加入了一个名叫:hover的伪类选择符,这个选择符可以让样式表在用户鼠标指针悬停在某个元素上时,影响元素的外观。这个伪类选择符在某种程度上可以帮我们解决问题,但在这里,我们要介绍jQuery.hover方法。这个方法可以让我们在鼠标指针进入元素和离开元素时,通过JavaScript来改变元素的样式——事实上是可以执行任意操作。

同前面介绍的简单事件方法不同,.hover()方法接受两个函数参数。

  • 第一个函数会在鼠标指针进入被选择的元素时执行,
  • 第二个函数会在鼠标指针离开该元素时触发。

我们可以在这些时候修改应用到按钮上的类,从而实现翻转效果,参见代码清单3-10。

1
2
3
4
5
6
7
8
9
10
11
$(document).ready(function () {
$('#switcher h3').hover(
//鼠标进入时触发
function () {
$(this).addClass('hover');
},
// 鼠标离开时触发
function () {
$(this).removeClass('hover');
});
});

这里,我们再次使用隐式迭代和事件上下文实现了简洁的代码。现在,当鼠标指针悬停在<h3>上时,我们都能看到如图3-6中所示的应用了类之后的效果。

而且,使用.hover()也意味着可以避免JavaScript中的事件传播(event propagation)导致的头痛问题。要理解事件传播的含义,首先必须搞清楚JavaScript如何决定由哪个元素来处理给定的事件。

3.3.1 事件的旅程

当页面上发生一个事件时,每个层次上的DOM元素都有机会处理这个事件。以下面的页面模型为例:

1
2
3
4
5
6
7
8
9
10
<div class="foo"> 
<span class="bar">
<a href="http://www.example.com/">
The quick brown fox jumps over the lazy dog.
</a>
</span>
<p>
How razorback-jumping frogs can level six piqued gymnasts!
</p>
</div>

当在浏览器中形象化地呈现这些由嵌套的代码构成的元素时,我们看到的效果如图3-7所示。

从逻辑上看,任何事件都可能会有多个元素负责响应。举例来说,如果单击了页面中的链接元素,那么<div><span><a>全都应该得到响应这次单击的机会。毕竟,这3个元素同时都处于用户鼠标指针之下。而<p>元素则与这次交互操作无关。
允许多个元素响应单击事件的一种策略叫做事件捕获①。在事件捕获的过程中,事件首先会交给最外层的元素,接着再交给更具体的元素。在这个例子中,意味着单击事件首先会传递给<div>,然后是<span>,最后是<a>,如图3-8所示。
这里有一张图片
另一种相反的策略叫做事件冒泡即当事件发生时,会首先发送给最具体的元素,在这个元素获得响应机会之后,事件会向上冒泡到更一般的元素。在我们的例子中,<a>会首先处理事件,然后按照顺序依次是<span><div>,如图3-9所示。
这里有一张图片
毫不奇怪,不同的浏览器开发者最初采用的是不同的事件传播模型。因而,最终出台的DOM标准规定应该同时使用这两种策略:

  • 首先,事件要从一般元素到具体元素逐层捕获,
  • 然后,事件再通过冒泡返回DOM树的顶层。

而事件处理程序可以注册到这个过程中的任何一个阶段。
为了确保跨浏览器的一致性,而且也为了让人容易理解,jQuery始终会在模型的冒泡阶段注册事件处理程序。因此,我们总是可以假定最具体的元素会首先获得响应事件的机会

3.3.2 事件冒泡的副作用

事件冒泡可能会导致始料不及的行为,特别是在错误的元素响应mouseovermouseout事件的情况下。
假设在我们的例子中,为<div>添加了一个mouseout事件处理程序。
当用户的鼠标指针退出这个<div>时,会按照预期运行mouseout处理程序。因为这个过程发生在顶层元素上,所以其他元素不会取得这个事件。
但是,当指针从<a>元素上离开时,<a>元素也会取得一个mouseout事件。然后,这个事件会向上冒泡到<span><div>,从而触发上述的事件处理程序。这种冒泡序列很可能不是我们所希望的。

避免冒泡问题

mouseentermouseleave事件,无论是单独绑定,还是.hover()方法中组合绑定,都可以避免这些冒泡问题。在使用它们处理事件的时候,可以不用担心某些非目标元素得到mouseovermouseout事件导致的问题。
刚才介绍的mouseout的问题说明了限制事件作用域的必要性。虽然.hover()可以处理这种特殊情况,但在其他情况下,我们可能还需要从空间(阻止事件发送到某些元素)和时间(阻止事件在某些时间段发送)上限制某个事件。

3.2 处理简单的事件

除了页面加载之外,我们也想在其他时刻完成某个任务。正如JavaScript可以让我们通过<body onload="">window.onload来截获页面加载事件一样,它对用户发起的事件也提供了相似的”挂钩” (hook) 。例如,鼠标单击(onclick) 、表单被修改(onchange)以及窗口大小变化(onresize)等。在这些情况下,如果直接在DOM中为元素指定行为,那么这些挂钩也会与我们讨论的onload一样具有类似的缺点。为此,jQuery也为处理这些事件提供了一种改进的方式。

3.2.1 简单的样式转换器

为了说明某些事件处理技术,我们假设希望某个页面能够基于用户的输入呈现出不同的样式。也就是说,允许用户通过单击按钮来切换视图,包括正常视图、将文本限制在窄列中的视图和适合打印的大字内容区视图。

用于样式转换器的HTML标记如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<div id="switcher" class="switcher">
<h3>Style Switcher</h3>
<button id="switcher-default">
Default
</button>
<button id="switcher-narrow">
Narrow Column
</button>
<button id="switcher-large">
Large Print
</button>
</div>

在与页面中其他HTML标记和基本的CSS组合以后,我们可以看到如图3-1所示的页面外观。

首先,我们来编写Large Print按钮的功能。此时,需要一点CSS代码来实现页面的替换视图:

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

然后,我们的目标就是为<body>标签应用large类。这样会导致样式表对页面进行重新格式化。按照第2章介绍的知识,添加类的语句如下所示:

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

但是,我们希望这条语句在用户单击按钮时执行(而不是像我们到目前为止看到的那样在页面加载后执行)。为此,我们需要引入.on()方法。通过这个方法,可以指定任何DOM事件,并为该事件添加一种行为。此时,事件是click,而行为则是由上面的一行代码构成的函数,参见代码清单3-1。

1
2
3
4
5
6
7
$(document).ready(function () {
// 选择id为switcher-large的元素,注册点击事件处理程序
$('#switcher-large').on('click', function () {
// 为body元素添加large类
$('body').addClass('large');
});
});

现在,当单击Large Print按钮时,就会运行函数中的代码,而页面的外观将如图3-2所示。

这里的全部操作就是绑定了一个事件。我们前面介绍的.ready()方法的优点在此也同样适用。多次调用.on()也没有任何问题,即可以按需为同一个事件追加更多的行为。

3.2.2 启用其他按钮

现在,Large Print按钮开始生效了。接下来,我们要以类似的方式处理其他两个按钮(DefaultNarrow) ,让它们也都执行各自的任务。这个过程很简单,即分别使用.on()为它们添加一个单击处理程序,同时视情况移除或添加类。完成之后的代码如代码清单3-2所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$(document).ready(function () {
// 当点击id为switcher-default的元素(按钮)时
$('#switcher-default').on('click', function () {
// 移除 body元素 上的narrow和large类
$('body').removeClass('narrow');
$('body').removeClass('large');
});
// 当点击id为switcher-narrow的按钮时
$('#switcher-narrow').on('click', function () {
// 移除 body元素 上的large类
$('body').removeClass('large');
// 给 body元素 添加narrow类
$('body').addClass('narrow');
});
// 当点击id为switcher-large的按钮时
$('#switcher-large').on('click', function () {
// 移除 body元素 上的narrow类
$('body').removeClass('narrow');
// 给 body元素 添加large类
$('body').addClass('large');
});
});

以下是配套的narrow类的CSS规则:

1
2
3
body.narrow .chapter { 
width: 250px;
}

现在,如果单击Narrow Column按钮,随着相应的CSS生效,文本会相应变化,如图3-3所示。
单击Default按钮将从<body>标签中同时移除两个类,让页面恢复为初始状态。

3.2.3 利用事件处理程序的上下文

虽然样式转换器的功能很正常,但我们并没有就哪个按钮处于当前使用状态对用户给出反馈。为此,我们的方法是在按钮被单击时,为它应用selected类,同时从其他按钮上移除这个类。selected类只是为按钮文本添加了粗体样式:

1
2
3
.selected { 
font-weight: bold;
}

为了实现类的变换,可以按照前面的做法,通过ID来引用每个按钮,然后再视情况为它们应用或移除类。不过,这一次我们要探索一种更优雅也更具扩展性的解决方案,这个方案利用了事件处理程序运行的上下文。
当触发任何事件处理程序时,关键字this引用的都是携带相应行为的DOM元素。前面我们谈到过,$()函数可以将DOM元素作为参数,而this关键字是实现这个功能的关键①。通过在事件处理程序中使用$(this),可以为相应的元素创建jQuery对象,然后就如同使用CSS选择符找到该元素一样对它进行操作。
知道了这些之后,我们可以编写出下面的代码:

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

把这行代码放到那3个事件处理程序中,就可以在按钮被单击时为按钮添加selected类。要从其他按钮中移除这个类,可以利用jQuery的隐式迭代特性,并编写如下代码:

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

这行代码会移除样式转换器中每个按钮的selected类。
我们还应该在文档就绪时把这个类添加到Default按钮上。因此,按照正确的次序放置它们,就可以得到代码清单3-3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$(document).ready(function () {
//选择id为switcher-default的DOM元素(按钮)
$('#switcher-default')
// 给按钮按钮添加selected类
.addClass('selected')
// 当点击该按钮时
.on('click', function () {
// 移除 body元素 上的narrow类
$('body').removeClass('narrow');
// 移除 body元素 上的large类
$('body').removeClass('large');
// 移除 所有按钮 上的selected类
$('#switcher button').removeClass('selected');
// 给 当前按钮 添加selected类
$(this).addClass('selected');
});
// 当点击id为switcher-narrow的DOM元素(按钮)时
$('#switcher-narrow').on('click', function () {
// 移除 body元素 上的large类
$('body').removeClass('large');
// 给 body元素 添加narrow类
$('body').addClass('narrow');
// 移除 所有按钮 上的selected类
$('#switcher button').removeClass('selected');
// 给 当前按钮 添加selected类
$(this).addClass('selected');
});
// 当点击id为switcher-large的DOM元素(按钮)时
$('#switcher-large').on('click', function () {
// 移除 body元素 上的narrow类
$('body').removeClass('narrow');
// 给 body元素 添加large类
$('body').addClass('large');
// 移除 所有按钮 上的selected类
$('#switcher button').removeClass('selected');
// 给 当前按钮 添加selected类
$(this).addClass('selected');
});
});

这样,样式转换器就会对用户给出适当的反馈了。
利用处理程序的上下文将语句通用化,可以使代码更高效。我们可以把负责突出显示的代码提取到一个单独的处理程序中,因为针对3个按钮的突出显示代码都一样,结果如代码清单3-4所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$(document).ready(function () {
$('#switcher-default')
// 给该按钮添加selected类
.addClass('selected')
// 点击该按钮时
.on('click', function () {
//移除 body元素 上的narrow类,然后给 body元素 添加large类
$('body').removeClass('narrow').removeClass('large');
});

// 点击该按钮时
$('#switcher-narrow').on('click', function () {
// 移除 body元素 上的large类,然后给 body元素 添加narrow类
$('body').removeClass('large').addClass('narrow');
});
// 点击该按钮时
$('#switcher-large').on('click', function () {
// 移除 body元素 上的narrow类,然后给 body元素 添加large类
$('body').removeClass('narrow').addClass('large');
});
// 给所有按钮注册事件处理程序
$('#switcher button').on('click', function () {
// 移除所有按钮上的selected类
$('#switcher button').removeClass('selected');
// 给触发点击事件的 该按钮添加 select类
$(this).addClass('selected');
});
});

这一步优化利用了我们讨论过的3种jQuery特性。

  • 第一,在通过对.on()的一次调用为每个按钮都绑定相同的单击事件处理程序时,隐式迭代机制再次发挥了作用。
  • 第二,行为队列机制让我们在同一个单击事件上绑定了两个函数,而且第二个函数不会覆盖第一个函数。
  • 最后,我们使用jQuery连缀能力将每次添加和移除类的操作压缩到了一行代码中。

3.2.4 使用事件上下文进一步减少代码

我们刚才的代码优化实际上是在做重构——修改已有代码,以更加高效和简洁的方式实现相同任务。 为寻找进一步重构的机会,下面再看一看绑定到每个按钮的行为。其中, .removeClass()方法的参数是可选的,即当省略参数时,该方法会移除元素中所有的类。利用这一点,可以把代码再改进得更简单一些,参见代码清单3-5。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$(document).ready(function () {
// 默认选中
$('#switcher-default').addClass('selected').on('click', function () {
// 当点击该按钮时,移除body元素上的所有类
$('body').removeClass();
});

$('#switcher-narrow').on('click', function () {
// 移除body元素上的所有类,然后再给body元素添加narrow类
$('body').removeClass().addClass('narrow');
});

$('#switcher-large').on('click', function () {
// 移除body元素上的所有类,然后再给body元素添加large类
$('body').removeClass().addClass('large');
});

$('#switcher button').on('click', function () {
// 移除 所有按钮 上的selected类
$('#switcher button').removeClass('selected');
// 给 触发该事件的按钮 添加select类
$(this).addClass('selected');
});
});

注意,为了适应更通用的移除类的操作,我们对操作顺序作了小小的调整——先执行.removeClass(),以便它不会撤销几乎同时执行的.addClass()
此时,在每个按钮的处理程序中仍然会执行某些相同的代码。这些代码也可以轻而易举地提取到通用的按钮单击处理程序中,如代码清单3-6所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// DOM加载完毕后执行
$(document).ready(function () {
// 给id为switcher-default的元素添加 selected类
$('#switcher-default').addClass('selected');
// 监听所有按钮的点击事件
$('#switcher button').on('click', function () {
// 移除 body元素 上的所有类
$('body').removeClass();
// 移除 所有按钮 上的selected类
$('#switcher button').removeClass('selected');
// 触发点击事件的这个按钮 添加selected类
$(this).addClass('selected');
});
// 当点击id为switcher-narrow的元素(按钮)时
$('#switcher-narrow').on('click', function () {
// 给 body元素 添加narrow类
$('body').addClass('narrow');
});
// 当点击id为switcher-large的元素(按钮)时
$('#switcher-large').on('click', function () {
// 给 body元素 添加large类
$('body').addClass('large');
});
});

这里要注意的是,必须把通用的处理程序转移到特殊的处理程序上方,因为.removeClass()需要先于.addClass()执行。而之所以能够做到这一点,是因为**jQuery总是按照我们注册的顺序来触发事件处理程序
最后,可以通过再次利用事件的执行上下文来完全消除特殊的处理程序。因为
上下文关键字this引用的是DOM元素**,而不是jQuery对象,所以可以使用原生的DOM属性来确定被单击元素的ID。因而,就可以对所有按钮都绑定相同的处理程序,然后在这个处理程序内部针对按钮执行不同的操作,参见代码清单3-7。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 当DOM加载完毕是执行
$(document).ready(function () {
// 默认选中id为switcher-default的按钮
$('#switcher-default').addClass('selected');

// 监听所有按钮的点击事件
$('#switcher button').on('click', function () {
// this.id将获取到按钮的id属性,值为:switcher-default,switcher-narrow,switcher-large
// this.id.split('-')将生成一个数组,
// 数组的第1个元素(this.id.split('-')[0])为switcher
// 数组的第2个元素(this.id.split('-')[1])为default,narrow,large这三种可能
var bodyClass = this.id.split('-')[1];
// 移除 body元素上的所有class,然后添加根据当前id动态生成的class
$('body').removeClass().addClass(bodyClass);
// 移除所有按钮上的selected类
$('#switcher button').removeClass('selected');
// 给当前按钮添加selected类
$(this).addClass('selected');
});
});

这里bodyClass变量的值根据按钮的ID属性的值来动态生成,根据单击的按钮不同,bodyClass变量的值可能是defaultnarrowlarge。这里与前面做法的不同之处在于,我们会在用户单击IDswitcher-default的按钮时,程序根据ID属性值switcher-default,得到default类,然后给<body>添加default类。虽然在这儿添加这个类也用不着,但与因此降低的复杂性相比,仅仅添加一个用不上的类名还是很划算的。

3.2.5 简写的事件

鉴于为某个事件(例如简单的单击事件)绑定处理程序极为常用,jQuery提供了一种简化事件操作的方式——简写事件方法,简写事件方法的原理与对应的.on()方法调用相同,可以减少一定的代码输入量。
例如,不使用.on()方法而使用.click()方法可以将前面的样式转换器程序重写为如代码清单3-8所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// DOM加载结束时调用
$(document).ready(function () {
// 一开始默认选中
$('#switcher-default').addClass('selected');

$('#switcher button').click(function () {
// 根据id属性值生成class属性值
var bodyClass = this.id.split('-')[1];
// body元素删除所有class,然后添加生成的class
$('body').removeClass().addClass(bodyClass);
// 所有按钮先移除掉selected类
$('#switcher button').removeClass('selected');
// 给触发事件的按钮添加selected类
$(this).addClass('selected');
});
});

其他blurkeydownscroll等标准的DOM事件,也存在类似前面这样的简写事件。这些简写的事件方法能够把一个事件处理程序绑定到同名事件上面。

3.2.6 显示和隐藏高级特性

假设我们想在不需要时隐藏样式转换器。隐藏高级特性的一种便捷方式,就是使它们可以折叠。因此,我们要实现的效果是:

  • 在标签上单击时,可以隐藏所有按钮,最后只剩一个标签;
  • 而再次单击标签时,则会恢复这些按钮。

这里所谓的高级特性就是指为页面提供样式切换能力的样式转换器

为了隐藏按钮,我们还需要另外一个类:

1
2
3
.hidden { 
display: none;
}

为实现这个功能,可以把当前的按钮状态保存在一个变量中,每当标签被单击时,通过检查这个变量的值就能知道应该向这些按钮中添加,还是要从这些按钮中移除.hidden类。
不过,jQuery也为我们提供了一个简便的toggleClass()方法,该方法能够根据相应的类是否存在而添加或删除类,参见代码清单3-9。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$(document).ready(function () {
$('#switcher h3').click(function () {
// 如果这些按钮中存在hidden类那就移除hidden类
// 如果这些按钮不存在hidden类就添加hidden类
$('#switcher button').toggleClass('hidden');
});
});

$(document).ready(function () {
// 选中默认按钮
$('#switcher-default').addClass('selected');
// 按钮点击事件处理程序
$('#switcher button').click(function () {
// 根据id属性值的生成class属性值
var bodyClass = this.id.split('-')[1];
// 移除body元素上的所有class属性,然后添加生成class属性
$('body').removeClass().addClass(bodyClass);
// 移除所有按钮上的值为selected的calss属性.
$('#switcher button').removeClass('selected');
// 触发事件的当前按钮添加selected类
$(this).addClass('selected');
});
});

在第一次单击后,所有按钮都会隐藏起来,如图3-4所示。
而第二次单击则又恢复了它们的可见性,如图3-5所示。

同样,这里我们依靠的仍然是jQuery隐式迭代能力,即一次就能隐藏所有按钮,而不需要使用包装元素(即不需要在这3个按钮外部再添加额外的标签如:<div> 。如果没有隐式迭代机制,那么想一次隐藏3个按钮,一种常见的方法就是隐藏包含这3个按钮的包装元素)。

3.1 在页面加载后执行任务

我们已经看到如何让jQuery响应网页的加载事件,$(document).ready()事件处理程序可以用来触发函数中的代码,但对这个过程还有待深入分析。

3.1.1 代码执行的时机选择

在第1章中,我们知道了$(document).ready()jQuery基于页面加载执行任务的一种主要方式。但这并不是唯一的方式,原生的window.onload事件也可以实现相同的效果。虽然这两个方法具有类似的效果,但是,它们在触发操作的时间上存在着微妙的差异,这种差异只有在加载的资源多到一定程度时才会体现出来。

window.onload时间和$(document).ready()的区别

当文档完全下载到浏览器中时,会触发window.onload事件。这意味着页面上的全部元素对JavaScript而言都是可以操作的,这种情况对编写功能性的代码非常有利,因为无需考虑加载的次序。
另一方面,通过$(document).ready()注册的事件处理程序,则会在DOM完全就绪并可以使用时调用。虽然这也意味着所有元素对脚本而言都是可以访问的,但是,却不意味着所有关联的文件都已经下载完毕。换句话说,当HTML下载完成并解析为DOM树之后,代码就可以运行。

加载样式与执行代码

为了保证JavaScript代码执行以前页面已经应用了样式,最好是在<head>元素中把<link rel="stylesheet">标签和<style>标签放在<script>标签前面。

举一个例子,假设有一个表现图库的页面,这种页面中可能会包含许多大型图像,我们可以通过jQuery隐藏、显示、移动或以其他方式操纵这些图像。如果我们通过onload事件设置界面,那么用户在能够使用这个页面之前,必须要等到每一幅图像都下载完成。更糟糕的是,如果行为尚未添加给那些具有默认行为的元素(例如链接) , 那么用户的交互可能会导致意想不到的结果。然而,当我们使用$(document).ready()进行设置时,这个界面就会更早地准备好可用的正确行为。

什么是加载完成

一般来说, 使用$(document).ready()要优于使用onload事件处理程序,但必须要明确的一点是,因为支持文件可能还没有加载完成,所以类似图像的高度和宽度这样的属性此时则不一定会有效。如果需要访问这些属性,可能就得选择实现一个onload事件处理程序(或者是使用jQueryload事件设置处理程序)。这两种机制能够和平共存。

第3章 事件

JavaScript内置了一些对用户的交互和其他事件给予响应的方式。为了使页面具有动态性和响应性,就需要利用这种能力,以便能够适时地应用我们学过的jQuery技术和本书后面讨论的一些技巧。虽然使用普通的JavaScript也可以做到这一点,但jQuery增强并扩展了基本的事件处理机制。它不仅提供了更加优雅的事件处理语法,而且也极大地增强了事件处理机制。
本章将学习以下内容:

  • 在页面就绪时执行JavaScript代码;
  • 处理用户事件,比如鼠标单击和按下键盘上的键;
  • 文档中的事件流,以及如何操纵事件流;
  • 模拟用户发起的事件。

2.8 小结

通过本章介绍的技术,读者应该掌握了:

  • 如何使用CSS选择符以不同方式在页面中选择元素集合,
  • 如何为嵌套列表中的顶级和非顶级项分别添加样式,
  • 如何使用属性选择符为不同类型的链接应用不同的样式,
  • 如何使用jQuery自定义的选择符:odd和:even,或高级的CSS选择符:nth-child()为表格添加条纹效果,
  • 如何使用连缀的jQuery方法突出显示某个表格单元中的文本。

到现在为止,我们使用了$(document).ready()事件为一组匹配的元素添加类。在下一章中,我们将探索基于用户发起的事件来添加类的技术。

延伸阅读

要了解有关选择符与遍历方法的完整介绍,请参考第9章或本书附录C,也可以参考jQuery官方文档

2.7 访问DOM元素

所有选择符表达式和多数jQuery方法都返回一个jQuery对象,而这通常都是我们所希望的,因为jQuery对象能够提供隐式迭代和连缀能力。
尽管如此,我们仍然有可能需要在代码中直接访问DOM元素。例如,可能需要为另一个JavaScript库提供一组元素的结果集合。或者可能不得不访问某个元素的标签名——通过DOM元素的属性。对于这些少见但合理的情形,jQuery提供了.get()方法。要访问jQuery对象引用的第一个DOM元素,可以使用.get(0)。因而,如果想知道带有id="my-element"属性的元素的标签名,应该使用如下代码:

1
var myTag = $('#my-element').get(0).tagName; 

为了进一步简化这些代码,jQuery还为.get()方法提供了一种简写方式。比如,可以将

1
var myTag = $('#my-element')[0].tagName; 

也就是说,可以在选择符后面直接使用方括号。显然,这种语法与访问DOM元素数组很相似,而使用方括号就好像剥掉jQuery的包装并直接露出节点列表,而方括号中的索引(这里的0)则相当于从中取出了原本的DOM元素。