10.2 事件委托

也许有读者还记得,为了实现事件委托,我们需要检测event对象的target属性,以便知道事件目标是不是我们想要触发行为的那个元素。事件目标,指的是接收到事件的那个最里面、最深层的元素。对于目前的示例程序而言,我们还面临着一个新的挑战:<div class="photo">元素不可能成为事件目标,因为它还包含着其他元素,比如图像和图像的信息。

我们需要使用.closest()方法,这个方法可以沿DOM树向上一层一层移动,直至找到与给定的选择符表达式匹配的那个元素。如果没有找到这个元素,那它就会像其他DOM遍历方法一样,返回一个“空的”jQuery对象。在这里,可以使用.closest()像下面这样从包含元素找到<div class="photo">

代码清单10-5

  1. //未完成的代码
  2. $(document).ready(function() {
  3. $('#gallery').on('mouseover mouseout', function(event) {
  4. var $target = $(event.target).closest('div.photo');
  5. var $details = $target.find('.details');
  6. var $related = $(event.relatedTarget)
  7. .closest('div.photo');
  8. if (event.type == 'mouseover' && $target.length) {
  9. $details.fadeTo('fast', 0.7);
  10. } else if (event.type == 'mouseout' && !$related.length) {
  11. $details.fadeOut('fast');
  12. }
  13. });
  14. });

注意,还需要把事件的类型由mouseentermouseleave改为mouseovermouseout。因为前两个事件只有在鼠标最先进入和最后离开<div id="gallery">时才会触发,而我们需要在鼠标进入这个包含<div>内部的任何照片时都触发处理程序。然而,使用后两个事件又会引入另外一个问题,即必须额外再检测 event 对象的 relatedTarget 属性,否则<div class="details">就会反复淡入淡出。即使额外添加了检测代码,如果你快速移动鼠标进出照片的话,结果仍然不令人满意,因为还是偶尔会有本应淡出的<div class="details">一直显示着。

10.2.1 使用jQuery的委托方法

在任务变复杂的情况下,手工管理事件委托可能会非常困难。好在,jQuery的.on()方法内置了委托管理能力,为我们扫除了这些障碍。利用这种能力,我们的代码可以变得像代码清单10-4那样简单,参见代码清单10-6。

代码清单10-6

  1. $(document).ready(function() {
  2. $('#gallery').on('mouseenter mouseleave', 'div.photo',
  3. function(event) {
  4. var $details = $(this).find('.details');
  5. if (event.type == 'mouseenter') {
  6. $details.fadeTo('fast', 0.7);
  7. } else {
  8. $details.fadeOut('fast');
  9. }
  10. });
  11. });

这里的选择符'#gallery'与代码清单10-5中相同,而事件类型则改成了代码清单10-4中的mouseentermouseleave。在把'div.photo'作为第二个参数的情况下,.on()方法会把this关键字映射为'#gallery'中与该选择符匹配的元素。

 有些开发人员使用.delegate().undelegate()方法,虽然语法不同,但作用是一样的。

10.2.2 选择委托的作用域

由于我们要操作的照片被包含在<div id="gallery">中,因此前面的例子将#gallery作为委托的作用域。实际上,照片元素的任何祖先元素都可以作为这个委托的作用域。比如,可以把处理程序绑定到document元素,因为它是页面中所有元素的祖先。

代码清单10-7

  1. $(document).ready(function() {
  2. $(document).on('mouseenter mouseleave', 'div.photo',
  3. function(event) {
  4. var $details = $(this).find('.details');
  5. if (event.type == 'mouseenter') {
  6. $details.fadeTo('fast', 0.7);
  7. } else {
  8. $details.fadeOut('fast');
  9. }
  10. });
  11. });

在安排事件委托时,把处理程序绑定到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

  1. (function($) {
  2. $(document).on('mouseenter mouseleave', 'div.photo',
  3. function(event) {
  4. var $details = $(this).find('.details');
  5. if (event.type == 'mouseenter') {
  6. $details.fadeTo('fast', 0.7);
  7. } else {
  8. $details.fadeOut('fast');
  9. }
  10. });
  11. })(jQuery);

因为我们没有等待整个文档就绪,所以可以确保所有<div class="photo">元素只要一呈现在页面上就可以应用mouseentermouseleave行为。

要想理解这样做的好处,可以想象把一个click事件处理程序绑定到一个链接上。假设这个处理程序要执行某些操作,同时还要阻止链接的默认动作(导航到其他页面)。如果我们等到文档就绪之后再绑定它,那很可能在绑定处理程序之前用户已经点击该链接离开了当前页面,这样就体验不到脚本提供的增强功能了。相比之下,把处理程序绑定到document,我们就不必扫描复杂的DOM结构而能够实现早绑定了。

 立即被调用的函数表达式

我们使用了立即调用的函数表达式(IIFE)来取代$(document).ready()。IIFE形同我们在第8章讨论过的闭包,可以在同一个页面中使用其他脚本时,避免可能的函数或变量的命名冲突(因为变量都被“限定”在了函数中)。