原文地址:http://www.learningjquery.com/2009/09/working-with-events-part-3-more-event-delegation-with-jquery
原文作者:  Louis-Rémi Babé

正如本系列的第一篇文章中描述的那样,事件代理是利用事件冒泡机制来避免多次绑定事件监听器的一种办法。jQuery1.3和即将到来的jQuery1.4有很多新的特性可以使你的网页中使用事件代理更容易。这篇指南的目标就是帮助你理解这些新特性是如何工作的。

从传统的事件监听到事件代理

既然一个DOM元素上发生的事件会被传递到它所有的祖先元素(或者说是DOM节点)上,那么一个事件监听器就能被绑定在众多DOM元素的单一的那个祖先节点上,而不用为所有的DOM元素每个都单独绑定事件监听器。

看下面的列表先,再考虑下面的问题:

<ul class="myList">
  <li class="red">The first item.</li>
  <li class="green">The second item.</li>
  <li class="yellow">The third item.</li>
  <li class="blue">The fourth item.</li>
</ul>
<p>Class of the last clicked item: <span id="display"> </span>
</p>

如果我们想知道被点击的列表元素的class,使用传统事件监听器的jQuery代码应该是这样写的:

$("li").click( function( event ) {
  $("#display").text(event.target.className);
});

传递给事件处理器的参数event对象有一个target属性,对应于被点击的元素。

使用事件代理的等价的代码是这样写的:

$("ul").click( function( event ) {
  $("#display").text(event.target.className);
});

试试看实际效果:

  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item:?

事件代理有两个主要的优势:
1. 使用一个事件监听器代替多个事件监听器明显更快
2. 任何页面加载完成之后动态新插入的元素也将会拥有相同的行为(在Working with Events, Part 1中已经演示过了)

这个转换成事件代理太简单了,因为我们原来的目标只是显示列表元素的class。使用前面的这个代码片断,无序列表本身的class(myList,点列表那个黑点的左边就能看出效果)也会被显示出来。因此,我们还应该判断一下点击的目标是个<li>元素。

$("ul").click( function( event ) {
  if(event.target.nodeName == "LI") {
 $("#display").text(event.target.className);
  }
});

试试看现在的效果:

  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item: ?

扫描event.target的祖先节点: .closest() 方法

操作像上面的例子一样如此简单的DOM文档,使用事件代理非常容易搞定。但是如果列表项里还包含子元素的话事情就会变得复杂许多,如下面这个列表元素:

<ul class="myList">
  <li class="red"><b>The <i>first <u>item</u></i></b>.</li>
  <li class="green"><b>The <i>second <u>item</u></i></b>.</li>
  <li class="yellow"><b>The <i>third <u>item</u></i></b>.</li>
  <li class="blue"><b>The <i>fourth <u>item</u></i></b>.</li>
</ul>
<p>Class of the last clicked item: <span id="display"> </span>
</p>

这种情况下,如果用户点击了 item 这个单词,event.target就会使<u>.这样的话就必须遍历当前目标元素的所有的祖先节点,来找到我们关心的元素:即<li>.

$("ul").click( function( event ) {
  var elem = event.target;
  while( elem.nodeName != "LI" && elem.parentNode) {
 elem = elem.parentNode;
  }
  if(elem.nodeName == "LI") {
 $("#display").text(event.target.className);
  }
});

注意如果用户点击到<li>的外面,我们需要在某个时候停止对祖先节点的循环,这个例子,我们会一直找到根节点(根节点没有父节点)。

jQuery1.3引入了一个.closest() 方法,可以用一行代码替换刚刚的循环。

$("ul").click( function( event ) {
 var $elem = $(event.target).closest("li");
 if($elem.length) {
 $("#display").text($elem.attr("class"));
 }
});

试试看用.closest()的效果是不是还正确:

  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item:?

传递给.closest()方法的参数是一个CSS选择器,这个选择器将会匹配第一个感兴趣的祖先节点。

上下文参数

可以这样说:当点击事件发生在<li>外面的时候,我们可以在查找的祖先节点在找到<ul>的时候就停止,而不用等到一直文档根节点才停止循环。jQuery1.4将会引入一个可选的context参数到.closest()方法来达到这个目的。

$("ul").click( function( event ) {
  $("#display").text($(event.target).closest("li", this).attr("class"));
});

这里的this即事件监听器绑定的<ul>元素。这个可选参数因为避免了查找可能不存在的祖先节点的资源浪费,从而提高了事件代理的性能。

用一行代码实现事件代理: .live()方法

jQuery1.3引入了一个.live()方法,这个方法绑定事件监听器的时候隐式调用.closest()方法来决定事件监听器是不是该被执行。

$("li").live("click", function( event ) {
  $("#display").text(
 $(event.currentTarget).attr("class")
  );
});

注意使用.live()方法的语法是不同的。看上去我们换回了传统的事件绑定的写法。但是最大的不同是,这样.live()方法写的事件监听器对于当前页面中已存在的元素和之后动态插入的元素都是有效的。当然没什么神秘的:.live()方法幕后只是把事件监听器绑定在文档根节点上,然后使用.closest()方法过滤任何event.target.

和传统的事件绑定语法相比一个值得注意的区别是,在事件处理器内部,我们不再使用event的target属性,而是使用currentTarget。事实上,event对象的target可能是一个<li>的子元素,而currentTarget属性则是被隐式的.closest()方法找到的元素。

正如在Event对象文档中将的,currentTarget属性应该是事件冒泡阶段的当前DOM元素,即:事件监听器检测到的元素总是事件监听器被绑定的元素。当使用.live()方法的时候,currentTarget总是文档根节点,你需要再一次用.closest()方法根据target找到感兴趣的祖先节点。

$("li").live("click", function( event ) {
  $("#display").text(
 $(event.target).closest("li").attr("class")
  );
});

如果使用jQuery1.3则必须要写成这样,如果使用jQuery1.4,currentTarget已经在内部被默认修改掉了,可以避免在事件处理器内部使用额外的.closest()方法来查找目标。

上下文参数

在jQuery1.3中所有使用live方式的事件监听器实质上都是绑定在DOM文档根节点上的。因为一个事件监听器检测到的所有事件都将触发执行隐式的.closest()方法,即使事件的目标元素不是我们所感兴趣的,所以这将影响网页的性能。

在jQuery1.4中.live()方法利用jQuery对象的上下文参数,它能够将事件监听器绑定到文档的一个具体的我们关心的元素上:

$("li", $("ul")[0]).live("click", function( event ) {
  $("#display").text(
 $(event.currentTarget).attr("class")
  );
});

上下文参数就是创建jQuery对象的第二个参数。为了对.live()方法有用,它必须被置成一个纯的DOM元素,这就是为什么$(“ul”)后面跟了个[0]的原因。Brandon Aaron 有一篇有用的文章详细解释了上下文参数

解除.live()绑定: .die()方法

就像.bind()和.unbind()互为补充允许事件的绑定和解除绑定,.live()也有与其互为补充的.die()方法。

和.bind()不一样的是.live()不支持使用 namespaced events

处理不冒泡的事件

事件代理对于有些事件不能实现,因为这些事件不会冒泡。jQuery1.4允诺.live()方法依然能够支持这些不冒泡的事件。这个已经实现,而且将会在接下来的第四篇文章中被涵盖到。

本文包含的脚本: