JS事件委托(事件代理)

 
利用 JS 事件冒泡动态为元素绑定事件的方法称为事件委托(Event Delegation,也称为“事件代理”),是 JavaScript 中最热门的技术之一。

事件委托就是把原本需要绑定在子元素上的事件(onclick、onkeydown 等)委托给它的父元素,让父元素来监听子元素的冒泡事件,并在子元素发生事件冒泡时找到这个子元素。

举个简单的例子,整个宿舍的同学都需要去取快递,一种方法是让他们一个个去取,另一种方法是把这件事委托给宿舍长,让宿舍长把所有人的快递都取回来,然后再根据收件人一一分发给宿舍的同学。在这里,我们可以将取快递看作一个事件;每个同学看作是需要绑定事件的 DOM 元素;宿舍长看作是这些 DOM 元素的父元素,事件需要绑定在这个父元素上;按照收件人分发快递的过程就是事件执行的过程。

为什么要使用事件委托

在 JavaScript 中,页面内事件处理程序的个数会直接影响页面的整体性能,因为每个事件处理程序都是对象,对象会占用内存,内存中的对象越多,页面的性能则越差。此外,事件处理程序需要与 DOM 节点进行交互,访问 DOM 的次数越多,引起浏览器重绘和重排的次数也就越多,从而影响页面的性能。

重绘是指当元素样式改变时,浏览器会根据元素的新样式重新绘制元素的外观。重排是指当 DOM 树的一部分发生变化时(例如元素尺寸改变),浏览器会重新创建 DOM 树。

当页面中很多表格或列表需要添加事件时,如果逐个添加那就太麻烦了,但是使用事件委托就能极大的减轻我们的工作量,同时也能提高页面的性能。

事件委托实现原理

事件委托是利用事件的冒泡原理来实现的,大致可以分为三个步骤:
  1. 确定要添加事件元素的父级元素;
  2. 给父元素定义事件,监听子元素的冒泡事件;
  3. 使用 event.target 来定位触发事件冒泡的子元素。

注意:使用事件委托时,并不是说把事件委托给随意一个父元素就行。因为事件冒泡的过程也需要消耗时间,距离越远,所需的时间也就越长,所有最好在直接父元素上使用事件委托。

假如我们要为 ul 列表下的每个 li 标签添加点击事件,如果不使用事件委托,最简单的办法就是使用循环来为每个 li 标签绑定事件,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</title>
</head>
<body>
    <ul id="list">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    <script>
        window.onload = function(){
            var the_ul = document.getElementById('list');
            var the_li = the_ul.getElementsByTagName('li');
            for( var i=0; i < the_li.length; i++ ){
                the_li[i].onclick = function(){
                    console.log(this.innerHTML)
                }
            }
        }
    </script>
</body>
</html>
通过上面的代码可以看出,要为每个 li 标签绑定点击事件,首先需要找到 ul 标签,然后通过 ul 标签找到所有 li 标签, 最后在通过遍历所有 li 标签来绑定事件。若使用事件委托的话,就会简单很多,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</title>
</head>
<body>
    <ul id="list">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    <script>
        window.onload = function(){
            var the_ul = document.getElementById('list');
            the_ul.onclick = function(e){
                console.log(e.target.innerHTML)
            }
        }
    </script>
</body>
</html>
通过代码可以看出,使用事件委托我们只需要为 ul 标签绑定事件,当 li 标签被点击时,由于事件冒泡的特性,会触发 ul 标签上的事件,我们只需要在事件中通过 event 对象中的 target 属性来找到被点击的 li 标签即可。不过这样做也有一个弊端,那就是当我们点击 ul 标签时,也会触发事件。

另外,如果我们需要动态的向 ul 标签中添加 li 标签,同时也需要在新添加的 li 标签中添加点击事件,就必须通过事件委托来实现了,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</title>
</head>
<body>
    <ul id="list" style="width: 100px;margin:0;float: left;">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    <button style="float:left;" id="addli">添加一个 li</button>
    <button style="float:left;" id="delli">删除一个 li</button>
    <script>
        window.onload = function(){
            var the_ul = document.getElementById('list');
            var the_li = the_ul.getElementsByTagName('li');
            var sum = the_li.length
            the_ul.onclick = function(e){
                console.log(e.target.innerHTML)
            };
            document.getElementById('addli').onclick = function (){
                var newli = document.createElement("li");
                newli.innerHTML = ++sum;
                the_ul.appendChild(newli);
            };
            document.getElementById('delli').onclick = function (){
                the_ul.firstElementChild.remove();
            };
        }
    </script>
</body>
</html>

事件委托的优点

1) 减小内存消耗

使用事件委托可以大量节省内存,减少事件的定义,通过上面的示例可以看出,要为 ul 标签下的所有 li 标签添加点击事件,如果分别为每个 li 标签绑定事件,不仅写起来比较繁琐,而且对内存的消耗也非常大。而使用事件委托的方式将点击事件绑定到 ul 标签上,就可以实现监听所有 li 标签,简洁、高效。

2) 动态绑定事件

在网页中,有时我们需要动态增加或移除页面中的元素,比如上面示例中动态的在 ul 标签中添加 li 标签,如果不使用事件委托,则需要手动为新增的元素绑定事件,同时为删除的元素解绑事件。而使用事件委托就没有这么麻烦了,无论是增加还是减少 ul 标签中的 li 标签,即不需要再为新增的元素绑定事件,也不需要为删除的元素解绑事件。

所以使用事件委托动态绑定事件可以减少很多重复工作的。

总结

要使用事件委托,需要保证事件能够发生冒泡,适合使用事件委托的事件有 click、mousedown、mouseup、keydown、keyup、keypress 等。需要注意的是,虽然 mouseover 和 mouseout 事件也会发生事件冒泡,但处理起来非常麻烦,所以不推荐在 mouseover 和 mouseout 事件中使用事件委托。

另外,对于不会发生事件冒泡的事件(例如 load、unload、abort、focus、blur 等),则无法使用事件委托。