清晰理解事件冒泡、事件捕获、事件代理

前言

事件流有事件捕获和事件冒泡两种方式,利用事件流的这个特色咱们能够设置事件代理。事件代理能够减小事件处理器的数量,提升 JS 脚本的性能。javascript

事件冒泡、事件捕获基础

事件处理器就是当浏览器(BOM)或者 HTML 某个元素(DOM)触发某个事件的时候所执行的函数,事件处理器也叫事件监听器(handler == listener)。常见的事件有 load / click / mouseover / mouseout 等,下面咱们都以 click 事件举例。html

HTML 元素是逐层嵌套的,对于这样一个结构,若是父级元素和子级元素同时都有事件处理器,浏览器会如何处理呢?好比点击父级元素的时候 alert(“parentEvent”),点击子级元素的时候 alert(“childEvent”)。当用户点击子元素的时候,由于子元素嵌套在父级元素内,因此点击子元素也必定会触发父级元素的事件处理器,可是这牵扯到一个触发顺序的问题,究竟是先弹出子元素的内容呢仍是先弹出父元素的内容呢?这就是事件流所讨论的内容。java

在开发第四代浏览器的时候,IE4 和 Netscape4 给出了两个彻底相反的思考方式。IE 支持事件冒泡,而 Netscape 支持事件捕获。那么什么又是事件冒泡,什么又是事件捕获呢?node

事件冒泡:子元素嵌套在父元素内部,点击子元素的时候必定同时表示点击了父元素,这个时候,先触发子元素的事件处理器,而后再触发父元素的事件处理器,若是父元素的父元素还有处理器,就一直向上触发,一直到 body 元素。就像鱼吐泡泡同样,从水下向水面走,每向上走一层就会查看这一层有没有事件处理器,若是有的话就会触发,若是没有的话就继续向上寻找,直到顶层的 body,才结束寻找事件。segmentfault

事件捕获:事件捕获则和事件冒泡正好相反,点击的时候从 body 往下找,若是父级元素有事件处理器就先触发父级元素的事件处理器,再向下一层,若是子级元素有的话就触发子级元素的事件处理器,直到这个点击位置的最底层,也就是咱们一般所说的 target。事件捕获就好像一块石头从水面向水下沉同样,若是这一层有事件处理器,就触发,没有就继续向下沉,到下层再查看是否有事件处理器,有的话就触发,没有的话继续向下,一直到最底层,这个石头就中止了。浏览器

能够理解,这两种思路都没有问题,只不过是思考方式不一样而已。只不过对于大多数人而言,事件冒泡彷佛更加容易接受和理解。正是因为有这样的两种事件流处理方式,因此若是你同时在父元素和子元素注册事件的时候,在 IE4 浏览器中会先触发子元素的事件,而后再触发父元素的事件。可是在 Netscape4 浏览器中,则会先触发父元素的事件再触发子元素的事件。函数

这样的两种事件流处理方式,也致使了浏览器兼容性的问题,开发者开发的同一个网页,在不一样的浏览器上却会产生不一样的效果。为了解决这个问题,DOM 2级规范统一了事件流的过程,总共分为三个阶段:事件捕获、在目标元素上,事件冒泡。DOM 2级规范将事件捕获和事件冒泡都收入本身的囊中,因此你能够在一个元素上同时注册事件捕获和事件冒泡,也就是说你能够选择父级元素事件处理器后触发,也能够选择先触发,甚至能够选择先触发父级元素的捕获事件,再触发父级元素的冒泡事件。听上去彷佛很复杂,那么 DOM 2 级规范到底如何实现这个效果的呢?性能

DOM 2级规范在全部的 HTML 元素上都定义了两个方法: addEventListener() 和 removeEventListener()。这两个方法都接收三个参数:事件名称、事件处理器函数和一个布尔值。前两个参数不做解释,最后一个布尔值则是决定这个事件的事件流处理方式是什么?默认状况下布尔值是 false,表示事件处理器是在冒泡阶段触发。当布尔值为 true 的时候则事件处理器是在捕获阶段触发。ui

var outer = document.getElementById("outer"),
    inner = document.getElementById("inner");

var outHandler = function() {
    alert("outer")
}
var innerHandler = function() {
    alert("inner")
}

//状况一:点击 inner 的时候,会先弹出 inner,后弹出 outer
outer.addEventListener("click",outHandler,false);
inner.addEventListener("click",innerHandler,false);

//状况二:点击 inner 的时候,会先弹出 outer,后弹出 inner
outer.addEventListener("click",outHandler,true);
inner.addEventListener("click",innerHandler,true);

那么有些同窗会说,个人事件处理器不是经过 DOM 2 级规范的方式添加的,是经过 DOM 0 级规范添加的,像下面这样:spa

var outer = document.getElementById("outer"),
    inner = document.getElementById("inner");

var outHandler = function() {
    alert("outer")
}
var innerHandler = function() {
    alert("inner")
}

outer.onclick = outHandler;
inner.onclick = innerHandler;

那这种状况是先弹出 outer 仍是先弹出 inner 呢?根据 DOM 0 级规范,这种添加事件处理器的方式会在冒泡阶段触发事件处理器。因此说若是你想控制事件处理器是在冒泡阶段触发仍是在捕获阶段触发,只能经过 DOM 2级规范规定的方式来添加事件处理器,若是你很肯定本身的事件处理器就是在冒泡阶段才能触发,那么你也可使用 DOM 0 级规范的方式。

事件冒泡、事件捕获进阶

当事件捕获和事件冒泡一块儿存在的状况,事件又是如何触发呢。

对于大多数状况,一个 HTML 元素只须要注册一个事件,可是有时候,一些 HTML 元素须要注册两个及以上的事件,并且颇有可能,一个元素既须要在捕获阶段注册事件,又须要在冒泡阶段注册事件。这种状况又该如何处理呢?

首先,若是想要在一个 HTML 元素上同时注册两个事件,必须用 DOM 2级规范的方式来进行事件注册,而不能用 DOM 0 级规范的方式,由于 DOM 0 级规范的方式添加两个事件的话,后面的事件处理器会覆盖前面的事件处理器,也就是说 DOM 0级的方式不管怎么处理,都只能触发一个事件处理器。可是 DOM 2级规范的方式则不一样,能够在一个元素上添加多个事件处理器,并且这多个事件处理器还能够按照捕获和冒泡不一样阶段添加。也就是说能够在捕获阶段添加多个事件处理器,也能够同时在冒泡阶段添加多个事件处理器。那么具体这些添加的事件到底按照什么样的顺序执行呢?

这个方面的内容,我参考的文章有详细的说明和解释,下面的内容主要就是摘录参考文章中的内容,你们能够一口气看完本文,一鼓作气,避免精力分散。看完本文有兴趣再去看参考文章,参考文章在本文的最后给出了连接。

这里记被点击的DOM节点为target节点

  • document 往 target节点,捕获前进,遇到注册的捕获事件当即触发执行
  • 到达target节点,触发事件(对于target节点上,是先捕获仍是先冒泡则捕获事件和冒泡事件的注册顺序,先注册先执行)
  • target节点 往 document 方向,冒泡前进,遇到注册的冒泡事件当即触发

总结下就是:

  • 对于非target节点则先执行捕获在执行冒泡
  • 对于target节点则是先执行先注册的事件,不管冒泡仍是捕获

 
 
<div id="s1">s1 <div id="s2">s2</div> </div> <script> s1.addEventListener("click",function(e){ console.log("s1 冒泡事件"); },false); s2.addEventListener("click",function(e){ console.log("s2 冒泡事件"); },false); s1.addEventListener("click",function(e){ console.log("s1 捕获事件"); },true); s2.addEventListener("click",function(e){ console.log("s2 捕获事件"); },true); </script>

这里大致分析下执行结果
点击s2,click事件从document->html->body->s1->s2(捕获前进)
这里在s1上发现了捕获注册事件,则输出"s1 捕获事件"
到达s2,已经到达目的节点,
s2上注册了冒泡和捕获事件,先注册的冒泡后注册的捕获,则先执行冒泡,输出"s2 冒泡事件"
再在s2上执行后注册的事件,即捕获事件,输出"s2 捕获事件"
下面进入冒泡阶段,按照s2->s1->body->html->documen(冒泡前进)
在s1上发现了冒泡事件,则输出"s1 冒泡事件"

事件冒泡与事件捕获应用:事件代理

    
red

    
yellow

    
blue

若是点击页面中的li元素,而后输出li当中的颜色,咱们一般会这样写:

(function(){ var color_list = document.getElementById('color-list'); var colors = color_list.getElementsByTagName('li'); for(var i=0;i<colors.length;i++){ colors[i].addEventListener('click',showColor,false); }; function showColor(e){ var x = e.target; console.log("The color is " + x.innerHTML); }; })();

利用事件流的特性,咱们只绑定一个事件处理函数也能够完成:

(function(){
var color_list = document.getElementById('color-list');
color_list.addEventListener('click',showColor,false);
function showColor(e){
    var x = e.target;
    if(x.nodeName.toLowerCase() === 'li'){
        console.log('The color is ' + x.innerHTML);
    }
}
})();

使用事件代理的好处不只在于将多个事件处理函数减为一个,并且对于不一样的元素能够有不一样的处理方法。假如上述列表元素当中添加了其余的元素(如:a、span等),咱们没必要再一次循环给每个元素绑定事件,直接修改事件代理的事件处理函数便可。

在处理事件代理的时候,事件 event 有两个比较特殊的属性,event.target 和 event.currentTarget,这两个属性又有什么区别呢?

event.target 是触发事件的元素,而 event.currentTarget 是事件绑定的元素。也就是说,大部分状况下,当使用事件代理时,event.target 是子元素,而 event.currentTarget 是父级元素。

    
red

    
yellow

    
blue

当点击任何一个 li 的时候,首先会弹出目标元素,也就是子元素的 id,而后才会弹出事件绑定元素的 id,也就是父级元素的 id。

参考文章

  1. JS中事件冒泡与捕获
  2. 事件冒泡、事件捕获和事件代理

相关文章
相关标签/搜索