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

事件流javascript

  JavaScript与HTML之间的交互是经过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可使用侦听器来预订事件,以便事件发生时执行相应的代码。
  事件流的起源:就是在浏览器发展到第四代的时候,浏览器开发团队遇到一个问题:页面的哪一部分会拥有某个特定的事件?要明白这个问题问的是什么,能够想象画在一张纸上的一组同心圆。若是你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的全部圆。也就是说若是单击了页面的某个按钮,同时也单击了按钮的容器元素,甚至单击了整个页面。不过呢,IE提出的是冒泡流,而网景提出的是捕获流。css

示例:html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件流</title>
    <style type="text/css">
        #content{width: 150px;height: 150px;background-color: red;}
        #btn{width: 80px;height: 80px;background-color: green;}
    </style>
</head>
<body>
    <div id="content">content
        <div id="btn">button</div>
    </div>

    <script type="text/javascript">
        var content = document.getElementById("content");
        var btn = document.getElementById('btn');
        btn.onclick = function(){
            alert("btn");
        };
        content.onclick = function(){
            alert("content");
        };
        document.onclick = function(){
            alert("document");
        }
    </script>
</body>
</html>

  若是点击容器#btn,则弹出的顺序是:btn-content-document;若是点击的是容器#content,则弹出的是content-document;若是点击的是document,弹出的是document。java

  由此能够看出JavaScript的事件流机制node

  前面说过,IE提出的是冒泡流,而网景提出的是捕获流,后来在W3C组织的统一之下,JS支持了冒泡流和捕获流,可是目前低版本的IE浏览器仍是只能支持冒泡流(IE6,IE7,IE8均只支持冒泡流),因此为了可以兼容更多的浏览器,建议你们使用冒泡流。浏览器

  JS事件流原理图以下:app

  

由此能够知道
  一、一个完整的JS事件流是从window开始,最后回到window的一个过程
  二、事件流被分为三个阶段(1~5)捕获过程、(5~6)目标过程、(6~10)冒泡过程函数

示例:性能

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style type="text/css">
    #wrapDiv, #innerP, #textSpan{
        margin: 5px;padding: 5px;box-sizing: border-box;cursor: default;
    }
    #wrapDiv{
        width: 300px;height: 300px;border: indianred 3px solid;
    }
    #innerP{
        width: 200px;height: 200px;border: hotpink 3px solid;
    }
    #textSpan{
        display: block;width: 100px;height: 100px;border: orange 3px solid;
    }
    </style>
</head>
<body>
     <div id="wrapDiv">wrapDiv
        <p id="innerP">innerP
            <span id="textSpan">textSpan</span>
        </p>
    </div>
    <script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 捕获阶段绑定事件
    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);

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

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

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", 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);

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

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

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>
</body>
</html>

  这个时候,若是点击一下textSpan这个元素,控制台会打印出这样的内容:测试

从上面所画的事件传播的过程可以看出来,当点击鼠标后,会先发生事件的捕获

  · 捕获阶段:首先window会获捕获到事件,以后documentdocumentElementbody会捕获到,再以后就是在bodyDOM元素一层一层的捕获到事件,有wrapDivinnerP

  · 目标阶段:真正点击的元素textSpan的事件发生了两次,由于在上面的JavaScript代码中,textSapn既在捕获阶段绑定了事件,又在冒泡阶段绑定了事件,因此发生了两次。可是这里有一点是须要注意,在目标阶段并不必定先发生在捕获阶段所绑定的事件,而是先绑定的事件发生,一会会解释一下。

  · 冒泡阶段:会和捕获阶段相反的步骤将事件一步一步的冒泡到window

上述代码中的两个属性:e.target和e.currentTarget

  targetcurrentTarget都是event上面的属性,target是真正发生事件的DOM元素,而currentTarget是当前事件发生在哪一个DOM元素上。

  能够结合控制台打印出来的信息理解下,目标阶段也就是 target == currentTarget的时候。我没有打印它们两个由于太长了,因此打印了它们的nodeName,可是因为window没有nodeName这个属性,因此是undefined

 

  那可能有一个疑问,咱们不用addEventListener绑定的事件会发生在哪一个阶段呢,咱们来一个测试,顺便再演示一下我在上面的目标阶段所说的目标阶段并不必定先发生捕获阶段所绑定的事件是怎么一回事。

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪一个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪一个阶段")
    };

    // 捕获阶段绑定事件
    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);

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

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

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获以前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", 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);

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

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

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

  控制台打印以下:

  

  · textSpan是被点击的元素,也就是目标元素,全部在textSpan上绑定的事件都会发生在目标阶段,在绑定捕获代码以前写了绑定的冒泡阶段的代码,因此在目标元素上就不会遵照先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。[在目标元素上就不会遵照先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。]
  · 因为wrapDiv不是目标元素,因此它上面绑定的事件会遵照先发生捕获后发生冒泡的规则。因此很明显用onclick直接绑定的事件发生在了冒泡阶段。

说一下事件绑定、解绑还有阻止事件默认行为:

事件绑定:

  一、直接获取元素绑定:

element.onclick = function(e){
        // ...
    };

  该方法的优势是:简单和稳定,能够确保它在你使用的不一样浏览器中运做一致;处理事件时,this关键字引用的是当前元素,这颇有帮组。

  缺点:只会在事件冒泡中运行;一个元素一次只能绑定一个事件处理函数,新绑定的事件处理函数会覆盖旧的事件处理函数;事件对象参数(e)仅非IE浏览器可用

  二、直接在元素里面使用事件属性

  三、W3C方法:

element.addEventListener('click', function(e){
        // ...
    }, false);

  优势:该方法同时支持事件处理的捕获和冒泡阶段;事件阶段取决于addEventListener最后的参数设置:false (冒泡) 或 true (捕获);在事件处理函数内部,this关键字引用当前元素;事件对象老是能够经过处理函数的第一个参数(e)捕获;能够为同一个元素绑定你所但愿的多个事件,同时并不会覆盖先前绑定的事件

  缺点:IE不支持,你必须使用IE的attachEvent函数替代。

  IE下的方法:

element.attachEvent('onclick', function(){
        // ...
});

  优势:能够为同一个元素绑定你所但愿的多个事件,同时并不会覆盖先前绑定的事件。
  缺点:IE仅支持事件捕获的冒泡阶段;事件监听函数内的this关键字指向了window对象,而不是当前元素(IE的一个巨大缺点);事件对象仅存在与window.event参数中;事件必须以ontype的形式命名,好比,onclick而非click;仅IE可用,你必须在非IE浏览器中使用W3C的addEventListener

  注意:不是意味这低版本的ie没有事件捕获,它也是先发生事件捕获,再发生事件冒泡,只不过这个过程没法经过程序控制。

解除事件:

element.removeEventListener('click', function(e){
        // ...
    }, false);

IE:

element.detachEvent('onclick', function(){
        // ...
});

阻止事件传播

  在支持addEventListener()的浏览器中,能够调用事件对象的stopPropagation()方法以阻止事件的继续传播。若是在同一对象上定义了其余处理程序,剩下的处理程序将依旧被调用,但调用stopPropagation()以后任何其余对象上的事件处理程序将不会被调用。不只能够阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。

  IE9以前的IE不支持stopPropagation()方法,而是设置事件对象cancelBubble属性为true来实现阻止事件进一步传播。

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪一个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪一个阶段")
    };

    // 捕获阶段绑定事件
    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);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
        // 在捕获阶段阻止事件的传播
        e.stopPropagation();
    }, true);

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

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获以前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", 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);

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

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

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

  

  实际上咱们点击的是textSpan,可是因为在捕获阶段事件就被阻止了传播,因此在textSpan上绑定的事件根本就没有发生,冒泡阶段绑定的事件天然也不会发生,由于阻止事件在捕获阶段传播的特性,e.stopPropagation()不多用到在捕获阶段去阻止事件的传播,你们就觉得e.stopPropagation()只能阻止事件在冒泡阶段传播。

阻止事件的默认行为

  e.preventDefault()能够阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其余页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,由于有的时候咱们并不但愿发生这些事情,因此须要阻止默认行为。

  IE9以前的IE中,能够经过设置事件对象的returnValue属性为false达到一样的效果。

function cancelHandler(event){
    var event=event||window.event;//兼容IE
    
    //取消事件相关的默认行为
    if(event.preventDefault)    //标准技术
        event.preventDefault();
    if(event.returnValue)    //兼容IE9以前的IE
        event.returnValue=false;
    return false;    //用于处理使用对象属性注册的处理程序
}

 

事件委托:

  在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的总体运行性能。致使这一问题的缘由是多方面的。首先,每一个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定全部事件处理程序而致使的DOM访问次数,会延迟整个页面的交互就绪时间。

  对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就能够管理某一类型的全部事件。例如,click事件会一直冒泡到document层次。也就是说,咱们能够为整个页面指定一个onclick事件处理程序,而没必要给每一个可单击的元素分别添加事件处理程序。

<ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
</ul>

  若是点击页面中的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;
        alert("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'){
            alert('The color is ' + x.innerHTML);
        }
    }
})();

冒泡仍是捕获?

  对于事件代理来讲,在事件捕获或者事件冒泡阶段处理并无明显的优劣之分,可是因为事件冒泡的事件流模型被全部主流的浏览器兼容,从兼容性角度来讲仍是建议你们使用事件冒泡模型。

 

事件委托还有一个好处就是添加进来的元素也能绑定事件:

没有使用事件委托:

<body>
 <ul id="thl">
   <li>001</li>
   <li>002</li>
   <li>003</li>
</ul>
<button onclick="fun()">touch</button>

<script>
    var thl= document.getElementById('thl');
    var aLi = thl.getElementsByTagName('li');
    for (var i = 0; i < aLi.length; i++) {
      aLi[i].onclick = fn;
    }
    
    function fn (){
      console.log(this.innerHTML);
    }

    function fun(){
        var node=document.createElement("li");
        var textnode=document.createTextNode("maomaoliang");
        node.appendChild(textnode);
        document.getElementById("thl").appendChild(node);
    }
</script>
</body>

使用了事件委托:

<script>
    var thl= document.getElementById('thl');
    thl.onclick = function(ev) {
        ev = ev || event;
        //兼容处理
        var target = ev.target || ev.srcElement;
      //找到li元素
        if (target.nodeName.toLowerCase() == 'li') {
              console.log(target.innerHTML);
         }
    };

    function fun(){
        var node=document.createElement("li");
        var textnode=document.createTextNode("maomaoliang");
        node.appendChild(textnode);
        document.getElementById("thl").appendChild(node);
    }
</script>
相关文章
相关标签/搜索