事件委托(事件代理)

出现事件委托的背景

事件委托有哪些好处,才会被现在人们大量的使用呢?

那么就得先说说事件的一些性能和使用的问题:

  1. 绑定事件越多,浏览器内存占用越大,严重影响性能。

  2. ajax的出现,局部刷新的盛行,导致每次加载完,都要重新绑定事件

  3. 部分浏览器移除元素时,绑定的事件并没有被及时移除,导致的内存泄漏,严重影响性能

  4. 大部分ajax局部刷新的,只是显示的数据,而操作却是大部分相同的,重复绑定,会导致代码的耦合性过大,严重影响后期的维护。

这些个限制,都是直接给元素事件绑定带来的问题,所以经过了一些前辈的总结试验,也就有了事件委托这个解决方案。

浏览器的事件流

浏览器的事件流分为1.捕获阶段,eventPhase是 1; 2.目标阶段,eventPhase是2; 3.冒泡阶段,eventPhase是 3;

捕获阶段是从父元素到目标元素

冒泡阶段是目标元素到父元素

事件委托定义

事件委托通俗地来讲,就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素;

就是在祖先级DOM元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件流的原理来触发绑定在祖先级DOM的事件。

当要对一系列的元素都添加响应事件时,可以只给父元素添加响应事件,然后利用事件冒泡对事件作出响应. 最普遍做法点击li打印事件

window.onload = function(){
    var oUl = document.getElementById("ul");
    var aLi = oUl.getElementsByTagName('li');
    for(var i=0;i<aLi.length;i++){
        aLi[i].onclick = function(){
            alert(123);
        }
    }
}
1
2
3
4
5
6
7
8
9

用事件委托这么做

window.onload = function(){
    var oUl = document.getElementById("ul1");
   oUl.onclick = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if(target.nodeName.toLowerCase() == 'li'){
        alert(123);
      alert(target.innerHTML);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

优点

  1. 页面的上的事件会影响事件的性能,事件过多,会导致网页的性能下降,采用事件委托,提高性能,可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。
  2. 可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适
  3. 不用担心某个注册了事件的DOM元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题

局限

  1. focus、blur之类的事件本身没有事件冒泡机制,所以无法委托;
  2. mousemove、mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合事件委托的
  3. 层级过多,冒泡过程中,可能会被某层阻止掉(建议就近委托)
  4. 事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。

额外拓展

1. 事件流描述的是从页面中接受事件的顺序。

2. 事件处理程序

  • HTML事件处理程序

  • DOM0级事件处理程序

  • DOM2级事件处理程序

    • DOM2级事件定义了两个方法:用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。

    它们都接收三个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。

  • IE事件处理程序

    • attachEvent()添加事件
    • detachEvent()删除事件

    这两个方法接收相同的两个参数:事件处理程序名称与事件处理函数

3. 事件对象

  • DOM中的事件对象

    • type 获取事件类型
    • target 事件目标
    • stopPropagation() 阻止事件冒泡
    • preventDefault() 阻止事件的默认行为
  • IE中的事件对象

    • type 获取事件类型
    • srcElement 事件目标
    • cancelBubble=true 阻止事件冒泡
    • returnValue=false 阻止事件的默认行为

4. 事件处理程序详解

  • HTML事件处理程序

它是写在HTML里的,是全局作用域。

<button onclick="alert('hello')"></button>
1

当我们需要使用一个复杂的函数时,将js代码写在这里,显然很不合适,所以有了下面这种写法:

<!-- 点击事件触发doSomething()函数,这个函数写在单独的js或<script>之中 -->
<button onclick="doSomething()"></button>
1
2

这样会出现一个时差问题,当用户在HTML元素出现一开始就进行点击,有可能js还没加载好,这时候就会报错。但我们可以将函数封装在try-catch来处理:

<button onclick="try{doSomething();}catch(err){}"></button>
1

同时,一个函数的改变,同时可能会涉及html和js的修改,这样是很不方便的,综上,才有了DOM0 级事件处理程序。

  • DOM0 级事件处理程序
<button id="btn">点击</button>

<script>
  var btn = document.getElementById('btn');
  btn.onclick = function() {
    alert('hello');
  }
</script>
1
2
3
4
5
6
7
8

可以看到button.onlick这种形式,这里事件处理程序作为btn对象的方法,是局部作用域。

所以我们可以用

btn.onclick = null;  // 删除指定的事件处理程序
1

如果我们尝试添加两个事件:

<button id="btn">点击</button>

<script>
  var btn = document.getElementById('btn');
  btn.onclick = function() {
    alert('hello');
  }
  
  btn.onclick = function() {
    alert('hello again');
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12

结果输出hello again,很明显第一个事件函数被第二个事件函数给覆盖了。所以,DOM0 级事件处理程序不能添加多个,也不能控制事件流到底是捕获还是冒泡。

  • DOM2 级事件处理程序(不支持IE)

    进一步规范之后,有了DOM2 级事件处理程序,其中定义了两个方法:

    • addEventListener:添加事件侦听器
    • removeEventListener:删除事件侦听器

    这两个方法都有三个参数:

    • 第一个参数:要处理的事件名(不带on的前缀才是事件名)
    • 第二个参数:作为事件处理程序的函数
    • 第三个参数:是一个boolean值,默认false表示使用冒泡机制,true表示捕获机制
    <button id="btn">点击</button>
    
    <script>
      var btn = document.getElementById('btn');
      
      btn.addEventListener('click', 'hello', false);
      btn.addEventListener('click', 'helloAgain', false);
      
      function hello() {
        alert('hello');
      }
      
      function helloAgain() {
        alert('hello again');
      }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    这时候,这两个事件处理程序都能够被触发,说明可以绑定多个事件处理程序,但是注意,如果定义了一模一样的监听方法,是会发生覆盖的,即同样的事件和事件流机制下相同方法只会触发一次。比如:

    <button id="btn">点击</button>
    
    <script>
      var btn = document.getElementById('btn');
      
      btn.addEventListener('click', 'hello', false);
      btn.addEventListener('click', 'hello', false);
      
      function hello() {
        alert('hello');
      }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    removeEventListener()的用法几乎和添加时的用法一模一样:

    <button id="btn">点击</button>
    
    <script>
      var btn = document.getElementById('btn');
      
      btn.addEventListener('click', 'hello', false);
      btn.removeEventListener('click', 'hello', false);
      
      function hello() {
        alert('hello');
      }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    这样的话,事件处理程序只会执行一次。

    但是要注意,如果同一个监听事件分别为“事件捕获”和“事件冒泡”注册了一次,一共两次,这两次事件需要分别移除。两者不会互相干扰。

    这时候的this指向该元素的引用。这里事件触发的顺序是添加的顺序。

  • IE事件处理程序

    对于IE来说,在IE9之前,你必须使用attachEvent而不是使用标准方法addEventListener。

    IE事件处理程序中有类似DOM2 级事件处理程序的两个方法:

    • attachEvent()
    • detachEvent()

    它们都接收两个参数:

    • 事件处理程序名称:如onclick、onmouseover,注意:这里不是事件,而是事件处理程序的名称,所以有on
    • 事件处理程序函数 之所以没有和DOM2 级事件处理程序中类似的第三个参数,是因为IE8及更早版本只支持冒泡事件流。
    <button id="btn">点击</button>
    
    <script>
        var btn = document.getElementById('btn');
        
        btn.attachEvent('onclick', hello);
        btn.detachEvent('onclick', hello);
        
        function hello() {
          alert('hello');
        }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    注意:这里事件触发的顺序不是添加的顺序而是添加顺序的想法顺序。

    使用attachEvent方法有个缺点,this的值会变成window对象的引用而不是触发事件的元素。

  • 事件对象

    事件对象是用来记录一些事件发生时的相关信息的对象。事件对象只有事件发生时才会产生,并且只能是事件处理程序内部访问,在所有事件处理函数运行结束后,事件对象就被销毁。

    • 2级DOM中的Event对象

    常用的属性和方法:

    • type: 获取事件类型
    • target:触发此事件的元素(事件的目标节点)
    • preventDefault():取消事件的默认操作,比如链接的跳转或者表单的提交,主要是用来阻止标签的默认行为
    • stopPropagation():冒泡机制下,阻止事件的进一步网上冒泡
    • IE中的Event对象

    常用的属性和方法:

    • type:事件类型
    • srcElement:事件目标
    • 取消事件的默认操作:returnvalue = false
    • 阻止事件冒泡:cancelBubble = false
  • 兼容性 事件对象也存在一定的兼容性问题,在IE8及以前版本之中,通过设置属性注册事件处理程序时,调用的时候并未传递事件对象,需要通过全局对象window.event来获取。解决方法如下:

    function getEvent(event) {
      event = event || window.event;
    }
    
    1
    2
    3

    如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation()方法。

    // 事件传播到p元素后,就不再向下传播了
    p.addEventListener('click', function (event) {
      event.stopPropagation();
    }, true);
    
    // 事件冒泡到p元素后,就不再向上冒泡了
    p.addEventListener('click', function (event) {
      event.stopPropagation();
    }, false);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    上面代码中,stopPropagation方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。

    注意:stopPropagation方法只会阻止事件的传播,不会阻止该事件触发p节点的其他click事件的监听函数。也就是说,不是彻底取消click事件。

    p.addEventListener('click', function (event) {
      event.stopPropagation();
      console.log(1);
    });
    
    p.addEventListener('click', function (event) {
      // 会触发
      console.log(2);
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    上面代码中,p元素绑定了两个click事件的监听函数。stopPropagation方法只能阻止这个事件向其他元素传播。因此,第二个监听函数会触发,输出结果会先是1,再是2。

    如果想要彻底阻止这个事件的传播,不再触发后面所有click的监听函数,可以使用stopImmediatePropagation方法。

    p.addEventListener('click', function (event) {
      event.stopImmediatePropagation();
      console.log(1);
    });
    
    p.addEventListener('click', function (event) {
      // 不会被触发
      console.log(2);
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9