日期:2014-05-16  浏览次数:20474 次

Javascript的事件委托

原文: http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/#

?

传统的事件处理

所谓事件委托即使用单个Event Handler来管理页面上特定的一类事件。这并不是什么新的idea,但对于Web应用的性能而言,是很重要的。比如,有时候,你可能会写如下的代码:

document.getElementById("help-btn").onclick = function(event){
    openHelp();
};

document.getElementById("save-btn").onclick = function(event){
    saveDocument();
};

document.getElementById("undo-btn").onclick = function(event){
    undoChanges();
}; 

它为页面上每一个可交互的元素提供了一个Event Handler。在一个小型的Web应用中,这可能问题不明显。但如果这是一个大型的网站,存在大量的交互,这么多的handler可能就会出现性能问题了。可以想象,为了处理数以百计的交互,我们需要将大量的DOM元素和时间处理代码绑定起来。这会占用大量的内存,进而导致整个应用运行变慢。而事件委托就可以用来解决这个问题。

事件冒泡和捕获(Event Bubbling / Capturing)

在早期Web开发的年代,浏览器厂商需要考虑一个问题:当用户在Web页面上点击一个区域,如何判断他实际正在进行互交互的元素。与之相关的是我们如何定义交互。在一个元素内点击的操作是有二义性的。如下图所示,如果用户点击了一个button元素,这个交互发生在button内部,同时我们也可以说它发生在body元素的内部,同时还在html元素的内部。

????? 在这个问题被提出来的时候,当时的两个主要浏览器(Netscape Navigator 和 Internet Explorer)采取了不同方式来解决这个问题。Netscape使用了一种叫做事件捕捉的机制,事件首先发生在DOM树上最高层的元素上,然后逐层往下传递到最深的被影响的元素上。所以,在上述例子中,点击事件首先由document元素来处理,然后是html元素,接下来是body,最后才是button元素。

????? Internet Explorer正好使用了完全相反的方式。他们称之为事件冒泡:最底层的元素应该首先接受到这个时间,然后才是它的父节点,祖父节点……一直到最高层的document元素。值得注意的是,虽然document和<html>元素对应的是页面上同一个可视元素,但它们在DOM树上却是父子关系。Event Bubbling的终点是document元素。

在定义DOM的时候,W3C显然注意到了两种不同方式各自的好处,因此我们现在使用的DOM Level 2事件机制中同时包含了这两种方式。一开始document元素接收到这个事件,进入捕获阶段,将其传递到最底层的元素。在这个元素的处理逻辑完成以后,进入冒泡阶段,将其传递回到document。在DOM Level2事件API中,addEventListener方法接受三个参数:要处理的事件名,事件处理函数,一个bool值(true表示在捕获阶段处理事件,而false表示在冒泡阶段处理事件)。多数Web开发人员经常会被告知使用false作为参数值,以保持和IE中的attachEvent方法相同的行为。比如:

//bubbling phase handler
document.addEventListener("click", handleClick, false);

//capturing phase handler
document.addEventListener("click", handleClick, true);

使用形如 element.onclick = function(){} 的方式来绑定事件的方式,即DOM Level 1的事件机制,是在冒泡阶段处理事件的(向前兼容性)。大多数浏览器(除了IE)都支持DOM Level 2事件机制,也就是说同时支持事件捕获和冒泡。IE仍然使用的是其专有的,仅支持冒泡方式的事件处理机制。

?

事件委托的关键就是使用事件冒泡的特性在DOM最高层的元素来处理它们(通常是document)。当然,不是所有的事件都支持冒泡(如onsubmit,onfocus,onblur这一类不和输入设备直接相关的时间等),但是鼠标和键盘事件都可以(包括onclick)。而且,很幸运的是,这些通常是你会感兴趣的。重新考虑之前的例子,你可以在document的事件处理函数中,检查事件的目标元素,并完成相应的逻辑。

document.onclick = function(event){
    //IE doesn't pass in the event object
    event = event || window.event;
    
    //IE uses srcElement as the target
    var target = event.target || event.srcElement;    

    switch(target.id){
        case "help-btn":
            openHelp();
            break;
        case "save-btn":
            saveDocument();
            break;
        case "undo-btn":
            undoChanges();
            break;
        //others?
    }
};

事件委托可以让同一事件的处理入口都汇集到一个函数中。所有的click事件现在都会先被单个函数所处理,并根据时间的对象元素委托给合适的函数。同样的,我们把它应用于mousedown , mouseup , mousemove , mouseover , mouseout , dblclick , keyup , keydown , 和keypress 事件. 这里要注意的一点就是,由于mouseover 和mouseout事件的特性(仅当鼠标移出容器时才会触发 mouseout),事件代理对它们来说并不是很实用。

当然,你也可以通过事件捕获来完成Event delegation,但这仅在支持事件捕获的浏览器中有效,也就是说IE不支持该方式。

好处

对于一个web应用来说,事件代理具有以下几点好处:

  1. 较少的事件处理函数.
  2. 占用更少的内存.
  3. 减少了Javascript和DOM之间的耦合.
  4. 在改变元素的innerHTML时,不用去移除绑定事件处理函数 .

相比于传统的事件处理方式,事件委托提高了大型web应用的总体性能。它对于Javascript库至关重要,如YUI 和jQuery 已经使用了这种机制。实现事件委托并不难,但是在用户界面的性能上的提高确实非常显著的。这在你将大量的事件处理函数合并为一个的时候,尤其明显。试试事件委托机制吧!

?