10.2 事件委托
也许有读者还记得,为了实现事件委托,我们需要检测event
对象的target
属性,以便知道事件目标是不是我们想要触发行为的那个元素。事件目标,指的是接收到事件的那个最里面、最深层的元素。对于目前的示例程序而言,我们还面临着一个新的挑战:<div class="photo">
元素不可能成为事件目标,因为它还包含着其他元素,比如图像和图像的信息。
我们需要使用.closest()
方法,这个方法可以沿DOM树向上一层一层移动,直至找到与给定的选择符表达式匹配的那个元素。如果没有找到这个元素,那它就会像其他DOM遍历方法一样,返回一个“空的”jQuery对象。在这里,可以使用.closest()
像下面这样从包含元素找到<div class="photo">
。
代码清单10-5
//未完成的代码
$(document).ready(function() {
$('#gallery').on('mouseover mouseout', function(event) {
var $target = $(event.target).closest('div.photo');
var $details = $target.find('.details');
var $related = $(event.relatedTarget)
.closest('div.photo');
if (event.type == 'mouseover' && $target.length) {
$details.fadeTo('fast', 0.7);
} else if (event.type == 'mouseout' && !$related.length) {
$details.fadeOut('fast');
}
});
});
注意,还需要把事件的类型由mouseenter
和mouseleave
改为mouseover
和mouseout
。因为前两个事件只有在鼠标最先进入和最后离开<div id="gallery">
时才会触发,而我们需要在鼠标进入这个包含<div>
内部的任何照片时都触发处理程序。然而,使用后两个事件又会引入另外一个问题,即必须额外再检测 event
对象的 relatedTarget
属性,否则<div class="details">
就会反复淡入淡出。即使额外添加了检测代码,如果你快速移动鼠标进出照片的话,结果仍然不令人满意,因为还是偶尔会有本应淡出的<div class="details">
一直显示着。
10.2.1 使用jQuery的委托方法
在任务变复杂的情况下,手工管理事件委托可能会非常困难。好在,jQuery的.on()
方法内置了委托管理能力,为我们扫除了这些障碍。利用这种能力,我们的代码可以变得像代码清单10-4那样简单,参见代码清单10-6。
代码清单10-6
$(document).ready(function() {
$('#gallery').on('mouseenter mouseleave', 'div.photo',
function(event) {
var $details = $(this).find('.details');
if (event.type == 'mouseenter') {
$details.fadeTo('fast', 0.7);
} else {
$details.fadeOut('fast');
}
});
});
这里的选择符'#gallery'
与代码清单10-5中相同,而事件类型则改成了代码清单10-4中的mouseenter
和mouseleave
。在把'div.photo'
作为第二个参数的情况下,.on()
方法会把this
关键字映射为'#gallery'
中与该选择符匹配的元素。
有些开发人员使用.delegate()
和.undelegate()
方法,虽然语法不同,但作用是一样的。
10.2.2 选择委托的作用域
由于我们要操作的照片被包含在<div id="gallery">
中,因此前面的例子将#gallery
作为委托的作用域。实际上,照片元素的任何祖先元素都可以作为这个委托的作用域。比如,可以把处理程序绑定到document
元素,因为它是页面中所有元素的祖先。
代码清单10-7
- $(document).ready(function() {
- $(document).on('mouseenter mouseleave', 'div.photo',
- function(event) {
- var $details = $(this).find('.details');
- if (event.type == 'mouseenter') {
- $details.fadeTo('fast', 0.7);
- } else {
- $details.fadeOut('fast');
- }
- });
- });
在安排事件委托时,把处理程序绑定到document
很方便。因为所有元素都是document
的后代,这样不用担心是否会选错容器。可是,这种方便也需要牺牲一定的性能。
如果DOM嵌套结构很深,事件冒泡通过大量祖先元素也会导致较大的性能损失。无论我们想观察哪个元素(把对应的选择符作为 .on()
的第二个参数传入),只要把处理程序绑定到document
,那么就需要检查任何地方发生的事件。在代码清单10-6中,光标进入任何元素都会引发jQuery检查当前元素是不是<div class="photo">
元素。在复杂的页面中,这样会导致性能损失,在较多使用委托的情况下性能损失更大。选择更具体的委托作用域可以有效减少这种开销。
10.2.3 早委托
先不管性能得失,有时候还会有其他原因让我们选择document
作为委托作用域。一般来说,只有当相应的DOM元素加载完毕,才能给它绑定事件处理程序。这就是为什么我们通常都把代码放到$(document).ready()
内部的原因。可是,document
元素是随着页面加载几乎立即就可以调用的,把处理程序绑定到document
不用再等到完整的DOM构建结束。即使脚本是放在文档的<head>
中引用的(我们的例子就是这样的),我们也可以马上在其中调用.on()
,参见代码清单10-8。
代码清单10-8
(function($) {
$(document).on('mouseenter mouseleave', 'div.photo',
function(event) {
var $details = $(this).find('.details');
if (event.type == 'mouseenter') {
$details.fadeTo('fast', 0.7);
} else {
$details.fadeOut('fast');
}
});
})(jQuery);
因为我们没有等待整个文档就绪,所以可以确保所有<div class="photo">
元素只要一呈现在页面上就可以应用mouseenter
和mouseleave
行为。
要想理解这样做的好处,可以想象把一个click
事件处理程序绑定到一个链接上。假设这个处理程序要执行某些操作,同时还要阻止链接的默认动作(导航到其他页面)。如果我们等到文档就绪之后再绑定它,那很可能在绑定处理程序之前用户已经点击该链接离开了当前页面,这样就体验不到脚本提供的增强功能了。相比之下,把处理程序绑定到document
,我们就不必扫描复杂的DOM结构而能够实现早绑定了。
立即被调用的函数表达式
我们使用了立即调用的函数表达式(IIFE)来取代$(document).ready()
。IIFE形同我们在第8章讨论过的闭包,可以在同一个页面中使用其他脚本时,避免可能的函数或变量的命名冲突(因为变量都被“限定”在了函数中)。