事件冒泡、事件捕获和事件委托
事件流
JavaScript与HTML之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器来预订事件,以便事件发生时执行相应的代码。
事件流的起源:就是在浏览器发展到第四代的时候,浏览器开发团队遇到一个问题:页面的哪一部分会拥有某个特定的事件?要明白这个问题问的是什么,可以想象画在一张纸上的一组同心圆。如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的所有圆。也就是说如果单击了页面的某个按钮,同时也单击了按钮的容器元素,甚至单击了整个页面。不过呢,IE提出的是冒泡流,而网景提出的是捕获流。
示例:
<!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。
由此可以看出JavaScript的事件流机制
前面说过,IE提出的是冒泡流,而网景提出的是捕获流,后来在W3C组织的统一之下,JS支持了冒泡流和捕获流,但是目前低版本的IE浏览器还是只能支持冒泡流(IE6,IE7,IE8均只支持冒泡流),所以为了能够兼容更多的浏览器,建议大家使用冒泡流。
JS事件流原理图如下:
由此可以知道
1、一个完整的JS事件流是从window开始,最后回到window的一个过程
2、事件流被分为三个阶段(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会获捕获到事件,之后document、documentElement、body会捕获到,再之后就是在body中DOM元素一层一层的捕获到事件,有wrapDiv、innerP。
· 目标阶段:真正点击的元素textSpan的事件发生了两次,因为在上面的JavaScript代码中,textSapn既在捕获阶段绑定了事件,又在冒泡阶段绑定了事件,所以发生了两次。但是这里有一点是需要注意,在目标阶段并不一定先发生在捕获阶段所绑定的事件,而是先绑定的事件发生,一会会解释一下。
· 冒泡阶段:会和捕获阶段相反的步骤将事件一步一步的冒泡到window
上述代码中的两个属性:e.target和e.currentTarget
target和currentTarget都是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直接绑定的事件发生在了冒泡阶段。
说一下事件绑定、解绑还有阻止事件默认行为:
事件绑定:
1、直接获取元素绑定:
element.onclick = function(e){ // ... };
该方法的优点是:简单和稳定,可以确保它在你使用的不同浏览器中运作一致;处理事件时,this关键字引用的是当前元素,这很有帮组。
缺点:只会在事件冒泡中运行;一个元素一次只能绑定一个事件处理函数,新绑定的事件处理函数会覆盖旧的事件处理函数;事件对象参数(e)仅非IE浏览器可用
2、直接在元素里面使用事件属性
3、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>