YvetteLau/Step-By-Step

浏览器事件代理机制的原理是什么?

YvetteLau opened this issue · 34 comments

浏览器事件代理机制的原理是什么?

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

了解事件代理机制首先要了解浏览器的事件流

浏览器的事件流分为事件捕获,处于目标阶段和事件冒泡,事件捕获阶段,事件由父级传递进子级,事件冒泡阶段由子级传递向父级,通过对父级事件的监听,就可以做到同一父级的子级事件的监听。

0uzu0 commented

事件代理(事件委托):就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
最普遍做法点击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);
        }
    }
}

用事件委托这么做

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.冒泡阶段.
捕获阶段是从父元素到目标元素,冒泡阶段是目标元素到父元素.
当要对一系列的元素都添加响应事件时,可以只给父元素添加响应事件,然后利用事件冒泡对事件作出响应.

事件委托原理:事件冒泡机制。
优点:1.可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。
2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适
缺点:事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。

(微信名:RUN)

事件流

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

IE的事件流是事件冒泡流

其他的事件流是事件捕获流

事件冒泡

事件冒泡,即事件最开始由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上转播至最不具体的节点(文档)。

事件捕获

事件捕获的**是不太具体的节点应该更早接收到事件,而最具体的节点最后接收到事件。

事件处理程序

  1. HTML事件处理程序
  2. DOM0级事件处理程序
  3. DOM2级事件处理程序
    • DOM2级事件定义了两个方法:用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。它们都接收三个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。
  4. IE事件处理程序
    • attachEvent()添加事件
    • detachEvent()删除事件
    • 这两个方法接收相同的两个参数:事件处理程序名称与事件处理函数

事件对象

  1. DOM中的事件对象
    1. type 获取事件类型
    2. target 事件目标
    3. stopPropagation() 阻止事件冒泡
    4. preventDefault() 阻止事件的默认行为
  2. IE中的事件对象
    1. type 获取事件类型
    2. srcElement 事件目标
    3. cancelBubble=true 阻止事件冒泡
    4. returnValue=false 阻止事件的默认行为

事件代理

事件代理则是利用了DOM事件流的特性,通过绑定一个节点来影响子或父的节点。

例子如下:

<!--
HTML结构
-->
<ul id="list">
    <li class="item-1">1</li>
    <li class="item-2">2</li>
    <li class="item-3">3</li>
    <li class="item-4">4</li>
    <li class="item-5">5</li>
</ul>
<p id="text">文本</p>

<!--
javascript 代码
-->
<script>
    'use strict';
    list.addEventListener('click', event => {
        if (event.target.className.indexOf('item-') === 0) {
            text.innerHTML = event.target.innerHTML;
        };
    });
</script>

事件放到父元素上去监听,判断每次点击的元素是不是就是想绑定的元素。

事件代理就是通过事件冒泡把所点击的元素绑定在他的父元素上。

lqzo commented

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

事件流

DOM(文档对象模型)结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素结点与根结点之间的路径传播,路径所经过的结点都会收到该事件,这个传播过程可称为DOM事件流。

因为历史的原因,IE最开始实现实现事件流的方式:冒泡事件(event bubbling),Netscape提出了另外一种事件流方式:事件捕获(event capturing),不同的浏览器实现上有一些差别,用起来就有些繁琐。幸好现代浏览器都实现了W3C制定的"DOM2级事件","DOM2级事件"把事件流分为三个阶段,捕获阶段、目标阶段、冒泡阶段。

JS事件捕获和事件冒泡原型图

这个问题回答起来,有些无从下手的感觉,事件流的概念不是很好描述,之前个人的理解很浅显。

javascript事件代理(delegate)原理解析

浏览器的代理机制原理的就是冒泡机制

  1. 就是给祖先级别的DOM元素绑定事件通过子孙级别的DOM元素触发机制!
    优点:
    1、可以大量节省内存的使用,减少注册事件。
    2、实现新增子对象时无需再次对其绑定事件。对于动态部分尤为适合

代理机制的原理是将元素的事件委托给它的父级或者更外级元素处理。
事件流描述的是从页面中接收事件的顺序。

浏览器事件代理机制的原理

浏览器事件代理本质是将事件委托给它的父级元素或者祖先元素。利用的是事件冒泡的原理。
事件流分为3个过程,一个捕获阶段、目标阶段、冒泡阶段。
比如点击子元素,该click事件最冒泡到父级或者其祖先。

不采用事件委托

function() {
    let ul = document.getElementById('ul');
    let lis = ul.getElementsByTagName('li');
    for(let i = 0, l = lis.length; i< l; i++) {
        llis[i].onClick = function() {
            alert('li element')
        }
    }

}

采用事件委托

将click事件代理到父元素

function() {
    let ul = document.getElementById('ul');
    ul.addEventListener('click', function(e){
        let ev = e || event;
        let target = ev.target || ev.srcElement;
        if(target.nodeName.toLowerCase === 'li') {
             alert(target)
        }
    })
}

浏览器事件代理机制

事件流

事件流描述的是从页面中接受事件的顺序。分为事件捕获和事件冒泡。

  • 事件冒泡: 时间开始时由最具体的元素接受,逐级向上传播, 当上级也有类似事件(click, onmouseOver等)也会跟着触发,可用stopPropagation 可以阻止冒泡和捕获进一步触发。顺便说一下,preventDefault 是阻止默认事件。
  • 事件捕获: 事件会从最外层开始发生,知道最具体的元素,也就是说假如父元素与子元素都绑定点击事件,那么最先发出的是父元素的时间,然后再传递到子元素

addEventListeb(event, function, useCapture), 第三个参数默认为false, 即使用时间冒泡,若为true则使用事件捕获的机制。

事件代理

通俗来讲是将元素的事件函数处理交由其他对象处理。它允许您避免向特定节点添加事件侦听器,我们这里所谈论的事件委托,与冒泡捕获流程相关,因此事件委托在此场景指的是子对象的处理事件交由父对象处理。

好像很多博客都是举的ul和li的例子:

<ul class="list" id="list">
  <li class="item" data-id="1">1</li>
  <li class="item" data-id="2">2</li>
  <li class="item" data-id="3">3</li>
  <li class="item" data-id="4">4</li>
  <li class="item" data-id="5">5</li>
</ul>

如果直接对li 节点进行click 监听,需要绑定多个事件监听器, 但如果利用冒泡只监听ul, 每次事件触发的时候,确定触发的目标是不是li的某个具体元素,这样,就可以省去建立多个监听器(尤其是对li 进行动态增减的时候)

document.getelementById('list').addEventtlist('click', function (e) {
    if (e.target && e.target.nodeName.toUpperCase === 'LI') {
        console.log(e.target.dataset.id)
    }
})

事件代理就是在祖先级dom元素上绑定一个事件,当触发子孙级元素的事件时,利用事件流的原理来触发绑定在祖先上的事件。
那么什么是事件流呢?当html产生一个事件,该事件会在元素节点与根结点之间的路径传播,路径所经过的节点都会收到该事件,这个传播的过程就叫做dom的事件流。

CCZX commented

基于事件冒泡,
我们在父节点上绑定事件,然后当子节点触发该事件之后会冒泡到父节点,父节点可以通过事件对象来判断子节点类型,然后触发相应事件。
好处:
避免绑定过多的事件,
动态添加的子节点不需要在绑定事件,
不用担心字节带你销魂后子节点的事件没有解绑

事件代理就是在祖先级DOM元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件流的原理来触发绑定在祖先级DOM的事件。事件代理是基于事件冒泡。
优点:1.可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。
2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适

事件流

事件从页面中接收事件的顺序
image.png

事件捕获

从window对象传到目标节点(上层到下层),成为捕获阶段

事件冒泡

从目标节点传到window对象(下层到上层)

###事件代理(事件委托)
原理:利用冒泡机制把所需要相应的事件绑定到外层
实现:

<body>
    <ul id="list">
        <li>1111111</li>
        <li>22222222</li>
        <li>43333</li>
    </ul>
    <script>
        document.getElementById('list').addEventListener('click', function (e) {
          // e.target IE8就有这个属性,所以IE8以上不需要写兼容
            if (e.target && e.target.nodeName.toUpperCase() === 'LI') {
                console.log(e.target)
            }
        })
    </script>
</body>
IE8打印出来的事件

IE8打印出来的event.png

  • 利用事件冒泡实现。指定一个事件处理程序,就可以管理某一类型的所有事件。
  • 事件冒泡会冒泡到 document层,在需要点击的元素的父节点或更高层元素添加事件处理程序进行事件处理,能够减少事件的绑定。
  • 利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件

事件捕获
由网景最先提出,事件会从最外层开始发生,直到最具体的元素,也就是说假如父元素与子元素都绑定有点击事件,又互相重叠,那么先出发的会是父元素的事件,然后再传递到子元素。
事件冒泡
由微软提出,事件会从最内从的元素开始发生,再向外传播,正好与事件捕获相反。
这两个概念都是为了解决页面中事件流的发生顺序,w3c采取了折中的办法,制定了统一的标准:先捕获再冒泡。
addEventListen(event, function, useCapture)添加事件的第三个参数默认值为false,即默认使用事件冒泡,若为true则使用事件捕获的机制,
例如:

 <div id="divFalse">
      <button type="button" id="butfalse">我是事件冒泡</button>
  </div>
  <div id="divTrue">
      <button type="button" id="butTrue">我是事件捕获</button>
  </div>
 // 事件冒泡
    let divFalse = document.getElementById("divFalse");
    let butfalse = document.getElementById("butfalse");
    divFalse.addEventListener("click", function (e) {
        console.log("divFalse")
    }, false);
    butfalse.addEventListener("click", function (e) {
        console.log("butfalse")
    });
结果:butfalse   divFalse 
// 事件捕获
 let divTrue = document.getElementById("divTrue");
    let butTrue = document.getElementById("butTrue");
    divTrue.addEventListener("click", function (e) {
        console.log("divTrue")
    }, true);
    butTrue.addEventListener("click", function (e) {
        console.log("butTrue")
    });
结果:divFalse  butfalse   

事件委托,又称为事件代理:通俗来讲是将元素的事件函数处理交由其他对象处理。它允许您避免向特定节点添加事件侦听器,我们这里所谈论的事件委托,与冒泡捕获流程相关,因此事件委托在此场景指的是子对象的处理事件交由父对象处理。
优点:
1.可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。
2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适
**缺点:**事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。

上代码

 <div id="ulList">
        <ul>
            <li class="item" data-id="1">li1</li>
            <li class="item" data-id="2">li2</li>
            <li class="item" data-id="3">li3</li>
            <li class="item">li4</li>
            <li class="item">li5</li>
        </ul>
        <p>点击内容 <span id="text"></span></p>
  </div>
   let ulList = document.getElementById("ulList");
   let text = document.getElementById("text");
   ulList.addEventListener("click", function (e) {
       if (e.target.dataset.id != undefined) {
           text.innerHTML = e.target.innerHTML;
        }
   })
<body>
    <ul id="list">
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
    <script>
        document.getElementById('list').addEventListener('click', function (e) {
            if (e.target && e.target.nodeName.toUpperCase() === 'LI') {
                console.log(e.target)
            }
        })
    </script>
</body>

减轻每个item都要绑定的开销

html元素在页面上可以看做一个同心圆,当点击一个区域的时候等同于点击了包括这个点击点的很多区域。不同元素只要包含了这个点击点的都会接受到这个事件。事件流是指事件被接收的顺序。事件流分为两种,事件冒泡事件捕获。事件冒泡是指子元素先接收事件,然后父元素再接收事件。事件捕获就是父元素先接受事件,然后子元素再接收事件。

事件代理就是利用了事件冒泡的机制,子元素的事件会冒泡到父元素,这样做的好处是:

  • 由父元素统一管理子元素的一类事件,减少绑定的回调函数的个数,节省内存
  • 当新增子元素的时候,无需单独处理子元素的事件
  • 当删除子元素的时候,也无需单独移除子元素的事件

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

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件,是对“事件处理程序过多”问题的解决方案。

在一个复杂的web应用程序中,如果对每个可单击的元素都添加事件处理程序,那么结果会有数不清的代码用于添加事件处理程序。此时,可以使用事件委托技术解决这个问题。使用事件委托,只需在DOM树中尽量最高的层次上添加一个事件处理程序。

事件流

事件从页面接受的顺序 捕获和冒泡都是为了解决事件流的在页面的发生顺序

事件捕获

从事件的最外层开始触发 最后到具体的元素

事件冒泡

从事件的最内部元素开始 到最外层 和捕获相反
w3c 制定一个折中的处理方式 先捕获后冒泡

addEventListener(e,fn,b) 默认是false 是冒泡 true 是捕获

如果想阻止冒泡 可以是用stopPropagetion() (preventDefault()阻止浏览器默认行为)

parent.addEventListener('click',(e)=>{ e.stopPropagation() e.preventDefault() //阻止浏览器的默认行为 console.log("父") },true) child.addEventListener('click',()=>{ console.log("子") },true)

事件委托

事件委托又叫事件代理把元素的事件处理函数 交给别的对象来处理
常见的把点击事件绑定子元素 移交给父元素 利用的就是冒泡和捕获

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • </ul>
    <script>
        document.querySelectorAll('.item').forEach(item => {
            item.onclick = (e) => {
                console.log(e.target)
    
            }
        })
        document.querySelector('.list').onclick = (e) => {
           if(e.target.matches('.item')){
               console.log(e.target)
           }
        }
    </script>
    

事件流

在说浏览器事件代理机制原理之前,我们首先了解一下事件流的概念,早期浏览器,IE采用的是事件捕获事件流,而Netscape采用的则是事件冒泡。"DOM2级事件"把事件流分为三个阶段,捕获阶段、目标阶段、冒泡阶段。现代浏览器也都遵循此规范。

事件代理机制的原理

事件代理又称为事件委托,在祖先级 DOM 元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件冒泡的原理来触发绑定在祖先级 DOM 的事件。因为事件会从目标元素一层层冒泡至 document 对象。

为什么要事件代理?

  1. 添加到页面上的事件数量会影响页面的运行性能,如果添加的事件过多,会导致网页的性能下降。采用事件代理的方式,可以大大减少注册事件的个数。

  2. 事件代理的当时,某个子孙元素是动态增加的,不需要再次对其进行事件绑定。

  3. 不用担心某个注册了事件的DOM元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题。

  4. 允许给一个事件注册多个监听。

  5. 提供了一种更精细的手段控制 listener 的触发阶段(可以选择捕获或者是冒泡)。

  6. 对任何 DOM 元素都是有效的,而不仅仅是对 HTML 元素有效。

addEventListener

addEventListener 接受3个参数,分别是要处理的事件名、实现了 EventListener 接口的对象或者是一个函数、一个对象/一个布尔值。

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);

options(对象) | 可选

  • capture: Boolean。true 表示在捕获阶段触发,false表示在冒泡阶段触发。默认是 false。

  • once:Boolean。true 表示listener 在添加之后最多只调用一次,listener 会在其被调用之后自动移除。默认是 false。

  • passive: Boolean。true 表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。默认是 false。

useCapture(Boolean) | 可选

useCapture 默认为 false。表示冒泡阶段调用事件处理程序,若设置为 true,表示在捕获阶段调用事件处理程序。

如将页面中的所有click事件都代理到document上:

document.addEventListener('click', function (e) {
    console.log(e.target);
    /**
    * 捕获阶段调用调用事件处理程序,eventPhase是 1; 
    * 处于目标,eventPhase是2 
    * 冒泡阶段调用事件处理程序,eventPhase是 3;
    */ 
    console.log(e.eventPhase);
    
}, false);

addEventListener 相对应的是 removeEventListener,用于移除事件监听。

事件代理是指把要绑定的事件绑定在父元素上面。利用的是事件冒泡的机制,在父元素上捕获事件。在工作中非常常用。
典型的例子就是:一些动态添加进来的元素,在绑定事件的时候还不存在,所以绑定不上,可以通过父元素代理的形式实现事件绑定。
学习到的:看了各位大佬的评论,原来还有一个重要的优点就是,减少了事件的注册,提升了性能(感觉性能提升要从每一个小细节做起)

为了性能优化,把子元素上所需绑定的事件统一绑定到指定的父元素上,当触发了子元素,实际上第一响应的是父元素,会将对应事件往下传到对应元素,该阶段不执行事件。到目标阶段执行事件。再开始冒泡阶段,向上层元素传播事件,如果有元素绑定了该事件同样触发。
优点:

  1. 减少了事件的注册,减少内存消耗。
  2. 当子元素为动态增加的时候,不需要再次绑定事件
  3. 只对它的父级(如果只有一个父级)这一个对象进行操作,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能

一般的事件机制是先从document到目标事件再从目标事件到document,事件一被称为捕获,事件2呗称为冒泡,为了节约性能我们一般采用冒泡的方式对事件来进行处理。
我们进行一个点击事件来弹出每个li标签的值:

  • 1
  • 2
var li = document.getElementById("ul").getElementByTagName("li"); for(let i=0; i

在此之前,我们先来了解下其他相关的一些概念,有助于我们理解浏览器事件代理机制的原理。

事件

JavaScript与HTML之间的交互是通过事件实现的。

事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器(或事件处理程序)来预定事件,以便事件发生时执行相应的代码。

像鼠标点击、页面或图像载入、键盘按键等操作。事件通常与函数配合使用,当事件发生时函数才会执行。

事件名称:clickmouseoverblur等(不带on)。

响应某个事件的函数就是事件处理程序(事件侦听器)。

事件处理程序函数名称:onclickonmouseoveronblur等。

事件流

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

IE和Netscape开发团队(网景)居然提出了差不多是完全相反的事件流的概念。

IE的事件流是事件冒泡流,而网景的事件流是事件捕获流。

事件冒泡

即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

<!DOCTYPE html>
<html>
<head>
  <title>Event Bubbling Example</title>
</head>
<body>
  <div id="myDiv">Click Me</div>
</body>
</html>

如果你单击了页面中的div元素,那么这个click事件会按照如下顺序传播:

  • <div>
  • <body>
  • <html>
  • document

事件捕获

事件捕获的**是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。

仍以上面的代码做例子,那么单击div元素就会以下列顺序触发click事件:

  • document
  • <html>
  • <body>
  • <div>

DOM事件流

DOM2级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件作出相应。

事件处理程序

  • HTML事件处理程序
  • DOM0 级事件处理程序
  • DOM2 级事件处理程序
  • IE事件处理程序
  • 跨浏览器的事件处理程序

HTML事件处理程序

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

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

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

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

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

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

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

DOM0 级事件处理程序

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

<script>
  var btn = document.getElementById('btn');
  btn.onclick = function() {
    alert('hello');
  }
</script>

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

所以我们可以用

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

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

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

<script>
  var btn = document.getElementById('btn');
  btn.onclick = function() {
    alert('hello');
  }
  
  btn.onclick = function() {
    alert('hello again');
  }
</script>

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

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

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

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

具体用法看:

https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener

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

  • 第一个参数:要处理的事件名(不带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>

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

<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>

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>

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

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

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

IE事件处理程序

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

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

  • attachEvent()
  • detachEvent()

它们都接收两个参数:

  • 事件处理程序名称:如onclickonmouseover,注意:这里不是事件,而是事件处理程序的名称,所以有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>

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

使用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;
}

事件代理机制的原理

事件代理又称事件委托,上面我们学习了事件会在冒泡阶段向上传播到父节点,而事件代理正是利用事件冒泡机制把一个或一组元素的事件委托到它的父节点或更外层的元素上,由父节点的监听函数统一处理多个子元素的事件。

看个例子:

var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
    if (event.target.tagName.toLowerCase() === 'li') {
        // some code
    }
});

上面代码中,click事件的监听函数定义在<ul>节点,但是实际上,它处理的是子节点liclick事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个<li>节点上定义监听函数。而且以后再动态添加节点,监听函数依然有效。

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

// 事件传播到p元素后,就不再向下传播了
p.addEventListener('click', function (event) {
    event.stopPropagation();
}, true);

// 事件冒泡到p元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
    event.stopPropagation();
}, false);

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

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

p.addEventListener('click', function (event) {
    event.stopPropagation();
    console.log(1);
});

p.addEventListener('click', function (event) {
    // 会触发
    console.log(2);
});

上面代码中,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. 当我们动态添加子元素时,不用再对其进行事件绑定,直接的减少了DOM操作
  3. 不用担心某个注册了事件的DOM元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题
  4. 允许给一个事件注册多个监听
  5. 提供了一种更精细的手段控制listener的触发阶段(可以选择捕获或者是冒泡)
  6. 对任何DOM元素都是有效的,而不仅仅是对HTML元素有效

局限性

  • focusblur之类的事件本身没有事件冒泡机制,所以无法委托;
  • mousemovemouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合事件委托的
  • 层级过多,冒泡过程中,可能会被某层阻止掉(建议就近委托)

参考:

前端小知识--JavaScript事件流

事件代理

在理解浏览器事件代理机制的原理前,先了解一下事件流,事件流描述的是从页面中接收事件的顺序。
IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕获流。由于老版本的浏览器不支持,因此很少有人使用事件捕获。建议使用事件冒泡。
在有特殊需要时再使用事件捕获。
以下面的html为例描述事件冒泡和事件捕获的区别

  <!DOCTYPE html>
    <html>
    <head>
        <title>Event Bubbling Example</title>
    </head>
    <body>
        <div id="myDiv">Click Me</div>
    </body>
</html>
  • 事件冒泡
    click 事件首先在

    元素上发生,而这个元素就是我们单击的元素。然后,click 事件沿 DOM 树向上传播,在每一级节点上都会发生,直至传播到 document 对象。
    image

  • 事件捕获
    在事件捕获过程中,document 对象首先接收到 click 事件,然后事件沿 DOM 树依次向下,一直传播到事件的实际目标,即

    元素。
    image

  • 为什么要采用事件代理
    对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事 件处理程序,就可以管理某一类型的所有事件。

  1. document 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序 (无需等待 DOMContentLoaded 或 load 事件)。换句话说,只要可单击的元素呈现在页面上,
    就可以立即具备适当的功能。
  2. 在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的 DOM 引用更少,
    所花的时间也更少。
  3. 整个页面占用的内存空间更少,能够提升整体性能。

如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不 清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示。

    var list = document.getElementById("myLinks");
    EventUtil.addHandler(list, "click", function(event){
        event = EventUtil.getEvent(event);
        var target = EventUtil.getTarget(event);
        switch(target.id){
            case "doSomething":
                document.title = "I changed the document's title";
                break;
            case "goSomewhere":
                location.href = "http://www.wrox.com";
                break;
      case "sayHi": 9 alert("hi");
break; }
});

事件流的概念:
1.IE采用的是事件铺获事件流,Netscape采用的是事件冒泡,“DOM2”是把事件流分成了三个阶段,捕获
阶段、目标阶段、冒泡阶段。现在浏览器的也采用这种模式,
2.事件代理机制的原理
在祖先级DOM上绑定一个事件,当触发子元素级DOM元素的事件时,利用事件冒泡的原理来触发绑定
在祖先DOM的事件,因为事件会一层层冒泡到document对象上。
3.为什么需要事件代理??

  1. 页面的上的事件会影响事件的性能,事件过多,会导致网页的性能下降,采用事件委托,提高性能
    2.事件代理的时候子元素是动态增加的,不需要再次对其进行事件绑定
    3.不用担心某个注册事件的DOM元素被移除后,可能无法回收其事件处理程序,我们把事件处理程序委托到更高层避免此问题
    4.允许一个事件注册多个监听
    5.提供了一种更精细的手段控制,listenner 的触发阶段(可以选择捕获或者冒泡)
    6.对任何DOM元素都是有效的
    addEventListener
    addEventListener 接受3个参数,分别是要处理的事件名、实现了 EventListener 接口的对象或者是一个
    函数、一个对象/一个布尔值。

    target.addEventListener(type, listener[, options]);
    target.addEventListener(type, listener[, useCapture]);
    options(对象) | 可选

    capture: Boolean。true 表示在捕获阶段触发,false表示在冒泡阶段触发。默认是 false。

    once:Boolean。true 表示listener 在添加之后最多只调用一次,listener 会在其被调用之后自动移除。默认是 false。

passive: Boolean。true 表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。默认是 false。

useCapture(Boolean) | 可选

useCapture 默认为 false。表示冒泡阶段调用事件处理程序,若设置为 true,表示在捕获阶段调用事件处理程序。

如将页面中的所有click事件都代理到document上:

document.addEventListener('click', function (e) {
console.log(e.target);
/**
* 捕获阶段调用调用事件处理程序,eventPhase是 1;
* 处于目标,eventPhase是2
* 冒泡阶段调用事件处理程序,eventPhase是 3;
*/
console.log(e.eventPhase);

}, false);
与 addEventListener 相对应的是 removeEventListener,用于移除事件监听。

浏览器事件代理机制原理

首先介绍下事件流吧
当html元素触发事件时,会将该事件沿着目标元素 与根节点的路径传播,该路径经过的元素都会触发该事件,这个过程称为事件流。

ie最早提出事件流的概念 认为事件流是由目标元素逐级向上传播 即事件冒泡

netspace提出刚好相反的意见 即事件是由根元素逐级向下传播至目标元素即事件捕获

现代浏览器都遵从w3c制定的dom2级事件流 即事件流分为三个阶段 事件捕获 目标阶段 事件冒泡

dom2级添加和移除事件的写法大概如下:

其他浏览器
addEventListener() 添加事件
removeEventListener() 移出事件
这两个方法接受三个参数
事件名 事件处理函数 boolean值 代表冒泡或捕获 默认为false true是捕获 false是冒泡

ie浏览器
attachEvent() 添加事件
detachEvent() 移出事件
这两个方法接受两个参数
事件名,事件处理函数

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

事件代理
事件代理其实就是利用了事件委托 将触发的事件绑定在目标元素父级或根节点 利用冒泡的方式 交由顶级对象同意管理。

事件代理的优点

  1. 减少了元素的事件绑定 提升了性能。

  2. 对于动态渲染的元素,不必担心事件没有添加或触发。

  3. dom2级事件 允许注册多个不同的监听。

事件代理

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

事件流

DOM结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素节点与根节点之间的路径传播,路径所经过的节点都会收到该事件,这个传播过程称为DOM事件流。

早期浏览器,IE采用的事件捕获事件流,Netscape采用的是事件冒泡。
'DOM2‘’级事件把事件流分为三个阶段,捕获阶段、目标阶段、冒泡阶段。

事件捕获

从事件的最外层开始触发 最后到具体元素

事件冒泡

从事件的最内部元素开始 到最外层 和捕获相反

该部分参考 点我查看这篇文章,
DOM事件流传播有三个阶段:(当一个事件发生后,会在不同的DOM之间传播)

  1. 从windows对象传导到目标节点,成为“捕获阶段”(capture phase)
  2. 在目标节点上出发,成为“目标阶段”(taget phase)
  3. 从目标节点传导回window对象,称为“冒泡阶段”(bubbling phase)
    一个事件在传播的过程中有三个阶段,由最外层的父节点一层层传递到最里面的子节点,先是捕获阶段,到目标阶段,再到冒泡阶段

优点:

  • 只需要将同类元素的事件委托给腹肌或者更外级的元素,不需要给所有元素都绑定事件,减少内存空间占用,提升性能
  • 动态新增的元素无需重新绑定事件

需要注意的地方

  • 事件委托的实现依靠事件冒泡,因此不支持事件冒泡的事件就不适合用事件冒泡。
    比较合适的事件有:click,mousedown,mouseup,keydown,keyup,keypress.不合适的包含mouseover和mouseout,因为经常要计算元素的位置。
  • 不是所有的事件绑定都适合使用事件委托,不恰当使用反而可能会导致不需要的元素也被绑定上了事件。