事件、事件冒泡、捕获、委托

852 阅读7分钟

事件流

事件

就是文档或浏览器窗口中发生的一些特定的交互瞬间。JavaScript与HTML之间的交互是通过事件实现的。可以使用侦听器来预订事件,以便事件发生时执行相应的代码。

事件流

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

事件冒泡

事件冒泡,会从当前触发的事件目标一级一级往上传递,依次触发,直到document为止。

事件捕获

事件捕获会从document开始触发,一级一级往下传递,依次触发,直到真正事件目标为止。

有如下代码:

<div id="box1">
  <div id="box2">
    <span id="span"></span>
  </div>
</div>
window.addEventListener("click", function(e){
    console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);

  document.addEventListener("click", function(e){
    console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);

  document.documentElement.addEventListener("click", function(e){
    console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);

  document.body.addEventListener("click", function(e){
    console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);

  document.getElementById("box1").addEventListener("click", function(e){
    console.log("box1 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);

  document.getElementById("box2").addEventListener("click", function(e){
    console.log("box2 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);

  document.getElementById("span").addEventListener("click", function(e){
    console.log("span 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);

  // 冒泡阶段绑定的事件
  window.addEventListener("click", function(e){
    console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

  document.addEventListener("click", function(e){
    console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

  document.documentElement.addEventListener("click", function(e){
    console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

  document.body.addEventListener("click", function(e){
    console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

  document.getElementById("box1").addEventListener("click", function(e){
    console.log("box1 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

  document.getElementById("box2").addEventListener("click", function(e){
    console.log("box2 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

  document.getElementById("span").addEventListener("click", function(e){
    console.log("span 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

当鼠标点击所看到的的span时,其实发生了一系列的事件传递。

可以想象一下,span实际上是被body“包裹”起来的,body是被html“包裹”起来的,html是被document“包裹”起来的,document是被window“包裹”起来的。所以,在你的鼠标点下去的时候,最先获得这个点击的是最外面的window,然后经过一系列传递才会传到最后的目标span,当传到span的时候,这个事件又会像水底的泡泡一样慢慢往外层穿出,直到window结束。

以上代码当点击span标签时,打印顺序如下:

window 捕获 SPAN undefined
document 捕获 SPAN #document
documentElement 捕获 SPAN HTML
body 捕获 SPAN BODY
box1 捕获 SPAN DIV
box2 捕获 SPAN DIV
span 捕获 SPAN SPAN
span 冒泡 SPAN SPAN
box2 冒泡 SPAN DIV
box1 冒泡 SPAN DIV
body 冒泡 SPAN BODY
documentElement 冒泡 SPAN HTML
document 冒泡 SPAN #document
window 冒泡 SPAN undefined

js事件流原理:

  • 一个完整的js事件流是从window开始,最后回到window的一个过程

  • 事件流被分为三个阶段:捕获过程,目标过程,冒泡过程

e.target和e.currentTarget的区别?

target 和 currentTarget 都是event上面的属性。

target :是指真正发生事件的DOM元素。

currentTarget:是指当前事件发生在哪个DOM元素上。

我们不用addEventListener绑定的事件会发生在哪个阶段?

我们在span上添加一个不用addEventListener绑定的事件。

document.getElementById("box1").onclick = function() {
    console.log('box onclick')
}
document.getElementById("span").onclick = function() {
    console.log('text span')
}

结果打印:

....
box1 捕获 SPAN DIV
box2 捕获 SPAN DIV
span 捕获 SPAN SPAN
text span
span 冒泡 SPAN SPAN
box2 冒泡 SPAN DIV
box1 onclick
box1 冒泡 SPAN DIV
....

所以,上面 box1 绑定的事件会遵守先发生捕获后发生冒泡的规则。所以很明显用onclick直接绑定的事件发生在了目标阶段。

值得注意的是,如果我们在绑定捕获代码之前写了绑定的冒泡阶段的代码,比如:


//...省略
document.getElementById("box2").addEventListener("click", function(e){
    console.log("box2 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);
document.getElementById("span").addEventListener("click", function(){
    console.log("textSpan 冒泡 在捕获之前绑定的")
  }, false);
  document.getElementById("span").addEventListener("click", function(e){
    console.log("span 捕获", e.target.nodeName, e.currentTarget.nodeName);
  }, true);
  // 冒泡阶段绑定的事件
  window.addEventListener("click", function(e){
    console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);
  
  //....省略
  
  document.getElementById("box2").addEventListener("click", function(e){
    console.log("box2 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);
  document.getElementById("span").addEventListener("click", function(e){
    console.log("span 冒泡", e.target.nodeName, e.currentTarget.nodeName);
  }, false);

控制台打印如下:

//....
box2 捕获 SPAN DIV
textSpan 冒泡 在捕获之前绑定的
span 捕获 SPAN SPAN
span 冒泡 SPAN SPAN
box2 冒泡 SPAN DIV
//....

span是被点击的元素,也是目标元素,所有在span上绑定的事件都会发生在目标阶段,如果,在绑定捕获代码之前写了绑定的冒泡阶段的代码,在目标元素上就不会遵守先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。

事件绑定的方法

直接获取元素绑定

xxx.onclick = function(e){}

该事件只会在事件冒泡中运行,一个元素只能绑定一个事件处理函数,后续绑定会覆盖前面的绑定。

直接在元素中使用事件属性

<div onclick="change"></div>

addEventListener

xxx.addEventListener('click',function(e){},false)

addEventListerner可以为同一个元素绑定你所希望的多个事件,同时并不会覆盖先前绑定的事件.

它接受三个参数,第一个是事件名,第二个是响应事件的回调函数,第三个是布朗类型,默认为false。

true,则表示该元素在事件的 “捕获阶段” 响应事件。

false,则表示该元素在 “冒泡阶段” 响应事件。

IE下的方法

xxx.attachEvent('onclick', function(){});

解除绑定的方法:


//非IE:

xxx.removeEventListener('click',function(){})

// IE:
xxx.detachEvent('onclick',function(){})

解决事件冒泡的方案

方法一:在相应的处理函数内,加入 event.stopPropagation(),终止事件的广播分发

在IE中使用 : event.cancelBubble = true;

   window.onload = function() {  
       document.getElementById("box1").addEventListener("click",function(event){  
           alert("您好,我是最外层div。");  
           event.stopPropagation();  
       });  
       document.getElementById("box2").addEventListener("click",function(event){  
           alert("您好,我是第二层div。");  
           event.stopPropagation();  
       });  
       document.getElementById("span").addEventListener("click",function(event){  
           alert("您好,我是span。");  
           event.stopPropagation();  
       });  
   }

如果在同一对象上定义了其他处理程序,剩下的处理程序将依旧被调用,但调用stopPropagation()之后任何其他对象上的事件处理程序将不会被调用。不仅可以阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。

方法二:事件包含最初触发事件的节点引用 和 当前处理事件节点的引用,那如果节点只处理自己触发的事件即可,不是自己产生的事件不处理。

event.target 引用了产生此event对象的dom 节点,而event.currrentTarget 则引用了当前处理节点,我们可以通过这 两个target 是否相等。

比如span 点击事件,产生一个event 事件对象,event.target 指向了span元素,span处理此事件时,event.currentTarget 指向的也是span元素,这时判断两者相等,则执行相应的处理函数。而事件传递给 div2 的时候,event.currentTarget变成 div2,这时候判断二者不相等,即事件不是div2 本身产生的,就不作响应处理逻辑。

<script type="text/javascript">  
    window.onload = function() {  
        document.getElementById("box1").addEventListener("click",function(event){  
            if(event.target == event.currentTarget)  
            {  
                alert("您好,我是最外层div。");  
            }  
        });  
        document.getElementById("box2").addEventListener("click",function(event){  
            if(event.target == event.currentTarget)  
            {  
                alert("您好,我是第二层div。");  
            }  
        });  
        document.getElementById("span").addEventListener("click",function(event){  
            if(event.target == event.currentTarget)  
            {  
                alert("您好,我是span。");  
                  
            }  
        });  
    }  
</script>

方法一在于取消事件冒泡,即当某些节点取消冒泡后,事件不会再传递;

方法二在于不阻止冒泡,过滤需要处理的事件,事件处理后还会继续传递;

事件委托

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。事件委托可以解决事件处理程序过多的问题。

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以统一管理某一类型的所有事件。通过判断事件的发生节点,然后做出相应的处理,减少监听次数,从而提升速度。

<body>
  <div id="div1">
    <div id="div2">
      <span id="span"></span>
    </div>
  </div>
</body>
<script type="text/javascript">  
   window.onload = function() {  
       document.getElementById("body").addEventListener("click",eventPerformed);  
   }  
   function eventPerformed(event) {  
       var target = event.target;  
       switch (target.id) {  
       case "span":   
           alert("您好,我是span。");  
           break;  
       case "div1":  
           alert("您好,我是第二层div。");  
           break;  
       case "div2":  
            alert("您好,我是最外层div。");  
           break;  
       }  
   }  
</script>

对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分,但是由于事件冒泡的事件流模型被所有主流的浏览器兼容,从兼容性角度来说还是建议大家使用事件冒泡模型。