JavaScript事件委托的技术原理

如今的JavaScript技术界里最火热的一项技术应该是‘事件委托(event delegation)’了。使用事件委托技术能让你避免对特定的每个节点添加事件监听器;相反,事件监听器是被添加到它们的父元素上。事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。基本概念非常简单,但仍有很多人不理解事件委托的工作原理。这里我将要解释事件委托是如何工作的,并提供几个纯JavaScript的基本事件委托的例子。

假定我们有一个UL元素,它有几个子元素:

<ul id="parent-list">

    <li id="post-1">Item 1</li>

    <li id="post-2">Item 2</li>

    <li id="post-3">Item 3</li>

    <li id="post-4">Item 4</li>

    <li id="post-5">Item 5</li>

    <li id="post-6">Item 6</li>

</ul>

 

我们还假设,当每个子元素被点击时,将会有各自不同的事件发生。你可以给每个独立的li元素添加事件监听器,但有时这些li元素可能会被删除,可能会有新增,监听它们的新增或删除事件将会是一场噩梦,尤其是当你的监听事件的代码放在应用的另一个地方时。但是,如果你将监听器安放到它们的父元素上呢?你如何能知道是那个子元素被点击了?

简单:当子元素的事件冒泡到父ul元素时,你可以检查事件对象的target属性,捕获真正被点击的节点元素的引用。下面是一段很简单的JavaScript代码,演示了事件委托的过程:

// 找到父元素,添加监听器...

document.getElementById("parent-list").addEventListener("click",function(e) {

    // e.target是被点击的元素!

    // 如果被点击的是li元素

    if(e.target && e.target.nodeName == "LI") {

        // 找到目标,输出ID!

        console.log("List item ",e.target.id.replace("post-")," was clicked!");

    }

});

 

第一步是给父元素添加事件监听器。当有事件触发监听器时,检查事件的来源,排除非li子元素事件。如果是一个li元素,我们就找到了目标!如果不是一个li元素,事件将被忽略。这个例子非常简单,ULli是标准的父子搭配。让我们试验一些差异比较大的元素搭配。假设我们有一个父元素div,里面有很多子元素,但我们关心的是里面的一个带有”classA” CSS类的A标记:

// 获得父元素DIV, 添加监听器...

document.getElementById("myDiv").addEventListener("click",function(e) {

    // e.target是被点击的元素

    if(e.target && e.target.nodeName == "A") {

        // 获得CSS类名

        var classes = e.target.className.split(" ");

        // 搜索匹配!

        if(classes) {

            // For every CSS class the element has...

            for(var x = 0; x < classes.length; x++) {

                // If it has the CSS class we want...

                if(classes[x] == "classA") {

                    // Bingo!

                    console.log("Anchor element clicked!");



                    // Now do something here....



                }

            }

        }



    }

});

 

上面这个例子中不仅比较了标签名,而且比较了CSS类名。虽然稍微复杂了一点,但还是很具代表性的。比如,如果某个A标记里有一个span标记,则这个span将会成为target元素。这个时候,我们需要上溯DOM树结构,找到里面是否有一个 A.classA 的元素。

因为大部分程序员都会使用jQuery等工具库来处理DOM元素和事件,我建议大家都使用里面的事件委托方法,因为这里工具库里都提供了高级的委托方法和元素甄别方法。

希望这篇文章能帮助你理解JavaScript事件委托的幕后原理,希望你也感受到了事件委托的强大用处!

(英文:davidwalsh.)

 

传统的事件处理

    事件委托就是在一个页面上使用一个事件来管理多种类型的事件。这并不是一个新的想法,但对于把握性能来说却很重要。通常情况,你会在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(); };
 

    这种传统的编码方式给每个元素分配单独的事件处理方法。对于交互少的站点来说,这样做是可以的。然而,对于大型的wen应用程序,当存在大量的事件处理的时候,就会显得反应迟钝。这里要关注的不是速度问题,而是内存占用问题。如果有数百个交互,DOM元素和JavaScript代码就会有数百个关联。web应用需要占用的内存越多,它的响应速度就越慢。事件委托能将这个问题减小。

 
事件冒泡及捕获
    要不是事件的下面这些属性,事件委托将成为可能。早期的web开发,浏览器厂商很难回答一个哲学上的问题:当你在页面上的一个区域点击时,你真正感兴趣的是哪个元素。这个问题带来了交互的定义。在一个元素的界限内点击,显得有点含糊。毕竟,在一个元素上的点击同时也发生在另一个元素的界限内。例如单击一个按钮。你实际上点击了按钮区域、body元素的区域以及html元素的区域。
JavaScript事件委托的技术原理

伴随着这个问题,两种主流的浏览器Netscape和IE有不同的解决方案。Netscape定义了一种叫做事件捕获的处理方法,事件首先发生在DOM树的最高层对象(document)然后往最深层的元素传播。在图例中,事件捕获首先发生在document上,然后是html元素,body元素,最后是button元素。

IE的处理方法正好相反。他们定义了一种叫事件冒泡的方法。事件冒泡认为事件促发的最深层元素首先接收事件。然后是它的父元素,依次向上,知道document对象最终接收到事件。尽管相对于html元素来说,document没有独立的视觉表现,他仍然是html元素的父元素并且事件能冒泡到document元素。所以图例中噢噢那个button元素先接收事件,然后是body、html最后是document。
 
JavaScript事件委托的技术原理
 
    在定义DOM的时候,W3C显然看到了这两种方案各自的优点,所以DOM Level 2的事件规范中同时定义了这两种方案。首先document元素获得事件,然后捕获阶段向与事件最相关的元素传播,当事件被此元素捕获后,再冒泡到document元素。addEventListener()方法接受三个参数:事件名,事件处理函数,一个用于指定事件在捕获阶段处理还是在冒泡阶段处理的布尔值。大部分的web开发者都会使用false作为第三个参数这样就跟IE中的attachEvent()一样了。
//bubbling phase handler 
document.addEventListener( "click", handleClick,  false);
//capturing phase handler 
document.addEventListener( "click", handleClick,  true);


通过冒泡实现事件委托
事件委托的关键就是在通过冒泡方式实现在最高层(通常是document)处理事件。不是所有的事件都支持冒泡,但是鼠标和键盘事件支持,并且这也是你所关心的。回顾下前面的例子,你可以通过在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?
        }
};

使用事件委托,数个事件处理函数可以使用一个函数来管理。所有的单击事件被委托给一个合适的函数来处理。同样,mousedownmouseupmousemovemouseovermouseoutdblclickkeyupkeydown,  和keypress事件也可以这样处理。但是,在事件委托中mouseover和mouseout的处理方法是不同的,当鼠标从一个元素移动到它的子元素内的时候,被认为是"out"。

 
注意:你也可以使用事件捕获来完成事件委托,但这只能用在支持事件捕获的非IE浏览器中。
 
优点
事件委托对于web应用程序的性能有如下几个优点:
1.需要管理的函数变少了
2.占用的内存少了
3.javascript代码和Dom结构之间的关联更少了
4.在改变DOM结构中的innerHTML时,不需要改动事件处理函数
 
从传统的事件处理方法转向事件委托提高了大型web应用的性能。正因为它如此的重要,一些类似于YUI、jQuey的javascript库也开始将事件委托应用在它们的核心接口中。实现事件委托是很轻松的,却能带来性能上很大的提高。尤其表现在你将数十个事件处理函数整合到一个函数里。试一下事件委托,你就不会再使用传统的事件处理方法了。

你可能感兴趣的:(JavaScript)