原文地址:http://www.learningjquery.com/2008/05/working-with-events-part-2
原文作者:  Karl Swedberg

我的上一篇文章中,描述了一个常见的问题,即事件对新加入HTML文档的元素表面上不工作的情况,不管这个元素是通过ajax方式还是修改DOM的方式加入进来的。我们也检查了一个解决此问题办法:事件代理。使用这种办法,我们将事件绑定到一直在DOM中的容器元素上然后再检查事件发生的目标元素。

Cloning Nodes

这一次,我们将一起看看解决此问题的另一个方法:重新绑定事件处理器。但是这么做之前,我应该要提一下,在jQuery1.2中事件处理器能够随着元素一起被克隆。看这个无序列表:

<ul id="list3" class="eventlist">
  <li>plain</li>
  <li class="special">special <button>I am special</button></li>
  <li>plain</li>
</ul>

我们可以拷贝一个<li class=”special”>然后插入到被拷贝的元素后面,同时取得任何绑定在原来元素上的事件处理器。jQuery提供的这个.clone()方法不会为我们把这任务都干了,它只会做元素的拷贝

$(document).ready(function() {
  $('#list3 li.special button').click(function() {
 var $parent = $(this).parent();
 $parent.clone().insertAfter($parent);
  });
});

试试看:

  • plain
  • special
  • plain

正如你所看到的,原按钮能够继续创建新的列表元素,但是“动态生成”的列表元素内的按钮不能创建新的按钮。

为了使得事件处理器也能被拷贝到新的元素上,我们只需要传递true给clone方法:

$(document).ready(function() {
  $('#list4 li.special button').click(function() {
 var $parent = $(this).parent();
 $parent.clone(true).append(' I\'m a clone!').insertAfter($parent);
  });
});

注:我加了个.append(‘ I\’m a clone!’)只是为了让发生的事情看上去更明显。
试试看:

  • plain
  • special
  • plain

当我们想拷贝已有元素及其事件处理器的时候使用.clone(true)尤为有效,但是还有很多其他和克隆不相关的情况我们也希望事件处理器能够被保留。

Re-binding基础

re-binding的概念相当直接:我们定义一个绑定事件处理器的函数,然后每当有新元素加入的时候调用这个函数。例如:上面的无序列表,我们首先创建一个addItem函数,这个函数注册一个事件处理器,这个事件处理器的意图是点击事件发生的时候,添加一个新的列表元素。

function addItem() {
  $('#list5 li.special button').click(function() {
 var $newLi = $('<li>special and new <button>I am new</button></li>');
 $(this).parent().after($newLi);
  });

下一步,当DOM加载完成的时候调用这个函数:

$(document).ready(function() {
  addItem();
});

最后我们在click事件处理器内再次调用这个函数,这样做的话,新加入的元素也能绑定事件处理器。

我们在多加一个事件处理器绑定在按钮上,但是不为这个做re-bind,为了试验的时候能看出区别。

为#list5内button做处理的全部的代码:

function addItem() {
  $('#list5 li.special button').click(function() {
 var $newLi = $('<li>special and new <button>I am new</button></li>');
 $(this).parent().after($newLi);
 addItem();
  });
}
$(document).ready(function() {
  addItem();
  // non-rebinding click handler ...
  $('#list5 li.special button').click(function() {
 $(this).after(' pressed');
  });
});

试试看(IE下还是不要点击了,死循环啊!!!):

  • plain
  • special
  • plain

可以看到每当第一个列表项的按钮被点击的时候”pressed”会被添加到这个列表项的后面,但是不会被添加到我们动态创建的列表元素的后面。另一方面,动态创建出来的按钮也能创建新的列表元素,因为函数已经被重新绑定过了。

然而,如果我们点击不只一次这个按钮,就会发现我们刚刚做的这一切产生了不受欢迎的结果。每点击一个按钮都会再绑定一次事件处理器,产生乘法的效果,即:第1次点击产生1个额外的列表项,第2次点击产生2个,第2次点击产生4个,循环下去。

Unbind和Bind

为了避免乘积型的绑定,我们可以先解除绑定然后再重新绑定。所以上面代码的第二行,我们将替换原来的

 $(‘#list5 li.special button’).click(function() {

$(‘#list6 li.special button’).unbind(‘click’).bind(‘click’,(function() {

注意这里的.bind()方法,这是jQuery通用的事件绑定处理方法。所有其他的诸如.click(), .blur(), .resize()等等都是等价的.bind(‘event’)的简写形式。

全部的代码如下,作为对比再看一下非重新绑定的事件处理器代码

function addItemUnbind() {
  $('#list6 li.special button')
 .unbind('click')
 .bind('click', function() {
   var $newLi = $('<li>special and new <button>I am new</button></li>');
   $(this).parent().after($newLi);
   addItemUnbind();
  });
}
$(document).ready(function() {
  addItemUnbind();
  // non-rebinding click handler
  $('#list6 li.special button').click(function() {
 $(this).after(' pressed');
  });
});

看看这次它怎么工作?

  • plain
  • special
  • plain

不幸的是,我们尝试解除函数addItemUnbind()绑定的方法过头了,把”non-rebinding”的点击处理器(甚至它连一次运行的机会都没得到)也解除了绑定,(证据可以从运行结果看出来,因为在”I am special”的按钮后面没有加上”pressed”的文本)。很明显,我们将不得不对我们需要解除绑定的东西更加小心。

使用事件名称空间

一种避免过度的解除绑定的方法是为绑定和解除绑定的相应点击事件加上“名称空间”。所以,不同于.bind('click').unbind('click)例如我们用这样的方法.bind('click.addit').unbind('click.addit').这是一个代码的示例除了使用有名称空间的事件之外和之前的代码基本一样。

function addItemNS() {
  $('#list7 li.special button')
 .unbind('click.addit')
 .bind('click.addit', function() {
   var $newLi = $('<li>special and new <button>I am new</button></li>');
   $(this).parent().after($newLi);
   addItemNS();
  });
}
$(document).ready(function() {
  addItemNS();
  // non-rebinding click handler
  $('#list7 li.special button').click(function() {
 $(this).after(' pressed');
  });
});

终于,我们有了期望得到的这一堆按钮和其被加上的正确的事件行为。(IE下还是不要点击了,这个也会产生死循环啊!!!):

  • plain
  • special
  • plain

请阅读Brandon Aaron的文章以了解更多关于事件名称空间的知识: Namespace Your Events.

Bonus:通过函数引用的方式解除绑定

如果你一直看到这里,证明你是个非常有耐心的人,所以我将特别奖励一个重新绑定的终极方法。除了使用事件名称空间之外,我们还可以在调用.bind()和.unbind()的方法时,增加第二个参数以引用函数。我们需要稍微调整一下代码防止递归,最终下面的代码应该是OK的.

function addItemFinal() {
 var $newLi = $('<li>special and new <button>I am new</button></li>');
 $(this).parent().after($newLi);
 $('#list8 li.special button')
   .unbind('click', addItemFinal)
   .bind('click', addItemFinal);
}
$(document).ready(function() {
$('#list8 li.special button').bind('click', addItemFinal);
  // non-rebinding click handler
  $('#list8 li.special button').click(function() {
 $(this).after(' pressed');
  });
});

注意这里在bind和unbind里的addItemFinal是没有括号的,因为我们只是在引用函数,而不是在做函数调用,让我们最后再测试一下吧:

  • plain
  • special
  • plain

实现此功能可选的jQuery插件

有三个很棒的插件能为我们干上面的事:

如果本文让你困惑的或者你只想要一个快速的测试过的解决方案,你一定要试试这几个插件中的一个,每个插件工作起来略有不同,但是都用样精彩。

更新:使用事件名称空间的添加删除例子

回复Nick Johns 下面的评论,我做了一个允许增加和删除的例子,代码基于我们所讲的”使用事件名称空间”的方法如下:

function addRemoveItemNS() {
  var $newLi = $('<li>special and new <button>I am new</button> <button>remove me</button></li>');
$('#list9 li.special')
  .find('button.addone')
 .unbind('click.addit')
 .bind('click.addit', function() {
   $(this).parent().after($newLi);
   addRemoveItemNS();
  })
  .end()
  .find('button.removeme')
  .unbind('click.removeit')
  .bind('click.removeit', function() {
 $(this).parent().remove();
  });
}
$(document).ready(function() {
  addRemoveItemNS();
});

我为初始的按钮家了一个”addone”的class,否则列表就和其他一样了,试试下面的例子:

  • plain
  • special
  • plain
<ul class="eventlist" id="list9">
<li>plain</li>
<li class="special">special <button class="addone">I am special</button></li>
<li>plain</li>
</ul>

本文所包含的脚本: