Javascript事件冒泡和事件捕获

Javascript事件

在浏览器上,某个行为的发生,都是以事件驱动的,即某个事件发生,然后做出相应的动作。 浏览器的事件表示的是某些事情发生的信号,详情参考w3school的Javascript Event

事件冒泡(event bubbling)

大家可以脑补一下冒泡的场景,即气泡从水底开始往上升,由深到浅,升到最上面,最后到达水面破裂。那么相对应的:这个气泡就相当于Javascript事件,而水则相当于我们的整个dom树;事件从dom树的底层通过一层一层往上传递,直至传递到dom的根节点。

案例分析

  • 我们通过一个简单的例案例来阐述冒泡原理:
    定义一个html,里面有三个简单的dom元素:parent,child,span,child包含span,parent包含child;当然它们都在body下:
1
2
3
4
5
6
7
<body id="body">  
<div id="parent">
<div id="child">
<span id="span">This is a span.</span>
</div>
</div>
</body>
  • 在这个基础上,我们实现下面的功能:
    body添加click事件监听,当body捕获到event事件时,打印出事件发生的时间和 触发事件的节点信息:

    1
    2
    3
    4
    5
    6
    window.onload = function() {  
    document.getElementById("body").addEventListener("click",eventHandler);
    }
    function eventHandler(event) {
    console.log("时间:"+new Date(event.timeStamp)+" 产生事件的节点:" + event.target.id +" 当前节点:"+event.currentTarget.id);
    }
  • 依次点击”This is span”,child,parent,body后,输出以下信息:

    1
    2
    3
    4
    时间:Fri Jul 24 2015 10:11:42 GMT+0800 (中国标准时间) 产生事件的节点:span  当前节点:body
    时间:Fri Jul 24 2015 10:11:46 GMT+0800 (中国标准时间) 产生事件的节点:child 当前节点:body
    时间:Fri Jul 24 2015 10:11:51 GMT+0800 (中国标准时间) 产生事件的节点:parent 当前节点:body
    时间:Fri Jul 24 2015 10:11:54 GMT+0800 (中国标准时间) 产生事件的节点:body 当前节点:body
  • 结果分析:
    无论是body,body的子元素parent,还是 parent的子元素child,还有span, 当这些元素被点击click时,都会产生click事件,并且body都会捕获到,然后调用相应的事件处理函数。就像水中的气泡从底往上冒一样,事件也会往上传递。
    事件冒泡传递的示意图如下所示:
    事件冒泡传递

  • 事件传递
    事件在传递过程中会有一些信息,这些信息是事件的组成部分:事件发生的时间(timeStamp)+ 事件发生的地点(pageX,pageY) + 事件的类型(type)+ 事件的当前处理者(target) + 其他信息,如下表描述:
    currentTarget:当前捕获节点的引用 ;
    target:产生事件的节点引用;
    type:当前事件类型,如click等;
    pageX:事件发生点当前文档的横坐标;
    pageY:事件发生点当前文档的纵坐标;
    which:针对键盘和鼠标事件,这个属性能捕捉到按钮或者键。

阻止事件冒泡

在实际的项目中,我们往往需要阻止事件冒泡现象。例如现在想实现这样的功能,在parent点击的时候,弹出 “你好,我是最外层parent”,点击child的时候,弹出 “你好,我是第二层child”;点击span的时候,弹出”您好,我是span”。
下面给出实现代码:

1
2
3
4
5
6
7
8
9
10
11
window.onload = function() {  
document.getElementById("parent").addEventListener("click",function(event){
alert("您好,我是最外层parent");
});
document.getElementById("child").addEventListener("click",function(event){
alert("您好,我是第二层child");
});
document.getElementById("span").addEventListener("click",function(event){
alert("您好,我是span");
});
}

当我们span的时候,会出来一个弹出框 “您好,我是span” 是的,确实弹出了。然而,不仅仅会产生这个对话框,会依次弹出”您好,我是第二层child”,”您好,我是最外层parent”。
这显然不是我们想要的! 我们希望的是点谁显示谁的信息而已。为什么会出现上述的情况呢? 原因就在于事件的冒泡,点击span的时候,span会把产生的事件往上冒泡,作为父节点的child和祖父节点的parent也会收到此事件,于是会做出事件响应,执行响应函数。现在问题是发现了,但是怎么解决呢?

  • 方法一
    我们来考虑一个形象一点的情况:水中的一个气泡正在从底部往上冒,而你现在在水中,不想让这个气泡往上冒,怎么办呢?——把它扎破!对,没错,就是消灭它,没了气泡,自然不会往上冒了。类似地,对某一个节点而言,如果不想它现在处理的事件继续往上冒泡的话,我们可以终止冒泡:
    在相应的处理函数内,加入event.stopPropagation(),终止事件的冒泡,这样事件停留在本节点,不会再往上冒了。修改后script:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    window.onload = function() {  
    document.getElementById("parent").addEventListener("click",function(event){
    alert("您好,我是最外层parent");
    event.stopPropagation();
    });
    document.getElementById("child").addEventListener("click",function(event){
    alert("您好,我是第二层child");
    event.stopPropagation();
    });
    document.getElementById("span").addEventListener("click",function(event){
    alert("您好,我是span");
    event.stopPropagation();
    });
    }

经过这样一段代码,点击不同元素会有不同的提示,不会出现弹出多个框的情况了。

  • 方法二
    事件包含最初触发事件的节点引用和当前处理事件节点的引用,那如果节点只处理自己触发的事件即可,不是自己产生的事件不处理。
    先前我们讲到event.target引用了产生此event对象的dom节点,而event.currrentTarget则引用了当前处理节点,我们可以通过判断这两个属性值是否相等。具体代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    window.onload = function() {  
    document.getElementById("parent").addEventListener("click",function(event){
    if(event.target == event.currentTarget){
    alert("您好,我是最外层parent");
    }
    });
    document.getElementById("child").addEventListener("click",function(event){
    if(event.target == event.currentTarget){
    alert("您好,我是第二层child");
    }
    });
    document.getElementById("span").addEventListener("click",function(event){
    if(event.target == event.currentTarget){
    alert("您好,我是span");
    }
    });
    }
  • 方法比较

    从事件传递上看:

    方法一取消事件冒泡,即当某些节点取消冒泡后,事件不会再传递;
    方法二不阻止冒泡,过滤需要处理的事件,事件处理后还会继续传递;

缺点分析:

方法一缺点:为了实现点击特定的元素显示对应的信息,要求每个元素的子元素也必须终止事件的冒泡传递,即跟别的元素功能上强关联,这样的方法会很脆弱。比如,如果span元素的处理函数没有执行冒泡终止,则事件会传到child上,这样会造成child的提示信息;
方法二缺点:方法二为每一个元素都增加了事件监听处理函数,事件的处理逻辑都很相似,即都有判断 if(event.target == event.currentTarget),这样存在了很大的代码冗余,现在是三个元素还好,当有元素多了又该怎么办呢?增加逻辑和代码的复杂度。
既然两种方法都存在缺点,那么我们该如何解决这个问题呢?那么就要用到javascript的 事件代理 了!

事件捕获(event capturing)

刚刚我们理解了事件冒泡,事件冒泡是一个从底层向上层元素冒泡的过程。事件捕获刚好相反,是从上层到底层元素传递的过程。 我们再来看一段代码:

1
2
3
4
5
6
7
8
window.onload = function() {  
document.getElementById("parent").addEventListener("click",function(event){
alert("您好,我是parent");
}, true);
document.getElementById("child").addEventListener("click", function(event){
alert("您好,我是child");
}, true);
}

1
2
3
<div id="parent"> 
<button id="child">child</button>
</div>

事件捕获现象:点击child,会先后出现“您好,我是parent”和“您好,我是child”两个弹出框。
事件捕获解析:当点击child时,先触发了它的父元素parent的点击事件,后触发child的点击事件,出现事件捕获现象。 事件捕获机制的示意图如下所示:
事件捕获机制

感谢您的阅读,有不足之处请在评论为我指出!

参考资料

[1]:解析Javascript事件冒泡机制
[2]:浅谈事件冒泡与事件捕获
[3]:理解JavaScript中的事件处理

版权声明:本文为博主原创文章,未经博主允许不得转载。本文地址 http://yangyuji.github.io/2015/05/24/javascript-event/