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

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和选择符表达式不匹配,则不会执行事件处理程序。