3.2 处理简单的事件

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个按钮的包装元素)。