聊聊事件冒泡与事件捕获
什么是事件?
事件是文档和浏览器窗口中发生的特定的交互瞬间。
什么是事件流:
事件流描述的是从页面中接受事件的顺序( 说白了就是解决页面中事件流发生顺序的问题。),但有意思的是,微软(IE)和网景(Netscape)开发团队居然提出了两个截然相反的事件流概念,IE的事件流是事件冒泡流(event bubbling),而Netscape的事件流是事件捕获流(event capturing)。
让我们先聊聊DOM0级事件与DOM2级事件
- DOM0
直接通过 onclick写在html里面的事件, 比如:
在标签内写onclick事件
<input onclick="alert(1)" />
在JS写onlicke=function(){}函数
1 document.getElementById("myButton").onclick = function () { 2 alert('thanks'); 3 }
- DOM2
主流浏览器DOM2级事件是通过以下两个方法用于处理指定和删除事件处理程序的操作:
- 添加事件 addEvenetListener ------ 可以为元素添加多个事件处理程序,触发时会按照添加顺序依次调用。
- 删除事件 removeEventListener ------- 不能移除匿名添加的函数。
它们都有三个参数:
- 第一个参数是事件名(如click)。
- 第二个参数是事件处理程序函数。 可以为匿名函数,也可以为命名函数(但如果需要删除事件,必须是命名函数)
- 第三个参数如果是true则表示在捕获阶段调用,为false表示在冒泡阶段调用。
使用DOM 2级事件处理程序的主要好处是可以添加多个事件处理程序,事件处理会按照他们的顺序触发,通过addEventListener添加的事件只能用removeEventListener来移除,移除时传入的参数与添加时使用的参数必须相同,这也意味着添加的匿名函数将无法移除,(注意:我们默认的第三个参数都是默认false,是指在冒泡阶段添加,大多数情况下,都是将事件处理程序添加到事件的冒泡阶段,这样可以最大限度的兼容各个浏览器)
匿名函数
1 //这是一个DOM 2级事件 添加事件最简单的方式(此时添加的是一个匿名函数) 2 <button>按钮</button> 3 <script> 4 var btn=document.querySelector('button'); 5 btn.addEventListener('click',function(){ 6 console.log('我是按钮') 7 },false) //当第三个参数不写时,也是默认为false(冒泡时添加事件) 8 </script>
命名函数
1 <button>按钮</button> 2 <script> 3 var btn=document.querySelector('button'); 4 btn.addEventListener('click',foo,false); 5 function foo(){ 6 console.log('我是按钮') 7 } 8 //其实操作就是把写在里面的函数拿到了外面,而在原来的位置用函数名来代替 9 </script>
看完以上的,我们再了解事件冒泡与捕获
第一种(事件冒泡)IE提出
IE提出的事件流叫做事件冒泡,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body onclick="bodyClick()"> 8 9 <div onclick="divClick()"> 10 <button onclick="btn()"> 11 <p onclick="p()">点击冒泡</p> 12 </button> 13 </div> 14 <script> 15 16 function p(){ 17 console.log('p标签被点击') 18 } 19 function btn(){ 20 console.log("button被点击") 21 } 22 function divClick(event){ 23 console.log('div被点击'); 24 } 25 function bodyClick(){ 26 console.log('body被点击') 27 } 28 29 </script> 30 31 </body> 32 </html>
接下来我们点击一下页面上的p元素,如下所示
正如上面我们所说的,它会从一个最具体的的元素接收,然后逐级向上传播, p=>button=>div=>body..........事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。
第二种(事件捕获)网景提出
事件捕获流的思想是不太具体的DOM节点应该更早接收到事件,而最具体的节点应该最后接收到事件,针对上面同样的例子,点击按钮,那么此时click事件会按照这样传播:(下面我们就借用addEventListener的第三个参数来模拟事件捕获流)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 9 <div> 10 <button> 11 <p>点击捕获</p> 12 </button> 13 </div> 14 <script> 15 var oP=document.querySelector('p'); 16 var oB=document.querySelector('button'); 17 var oD=document.querySelector('div'); 18 var oBody=document.querySelector('body'); 19 20 oP.addEventListener('click',function(){ 21 console.log('p标签被点击') 22 },true); 23 24 oB.addEventListener('click',function(){ 25 console.log("button被点击") 26 },true); 27 28 oD.addEventListener('click', function(){ 29 console.log('div被点击') 30 },true); 31 32 oBody.addEventListener('click',function(){ 33 console.log('body被点击') 34 },true); 35 36 </script> 37 </body> 38 </html>
同样我们看一下后台的打印结果
和冒泡流完全相反,从最不具体的元素接收到最具体的元素接收事件 body=>div=>button=>p
-
事件代理
在实际的开发当中,利用事件流的特性,我们可以使用一种叫做事件代理的方法。
<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当中的颜色,我们通常会这样写:
1 var list_li = document.getElementsByTagName('li'); 2 for (var i = 0; i < list_li.length; i++) { 3 list_li[i].addEventListener('click',foo,false); 4 } 5 function foo (e) { 6 let x = e.target; 7 console.log(x.innerHTML) 8 }
利用事件流的特性,我们只绑定一个事件处理函数也可以完成:
1 var list_ul = document.getElementById('color-list'); 2 list_ul.addEventListener('click', foo, false); 3 function foo (e) { 4 let x = e.target; 5 if (x.nodeName == 'LI') { 6 console.log(x.innerHTML) 7 } 8 }
-
冒泡还是捕获?
对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分,但是由于事件冒泡的事件流模型被所有主流的浏览器兼容,从兼容性角度来说还是建议大家使用事件冒泡模型。
IE浏览器兼容
IE浏览器对addEventListener兼容性并不算太好,只有IE9以上可以使用。
要兼容旧版本的IE浏览器,可以使用IE的attachEvent函数
object.attachEvent(event, function)
两个参数与addEventListener相似,分别是事件和处理函数,默认是事件冒泡阶段调用处理函数。并且由于IE浏览器只支持事件冒泡,所以添加的程序都被添加到冒泡阶段。要注意的是,写事件名时候要加上"on"前缀("onload"、"onclick"等)。
区别
addEventListener与attachEvent除了参数个数以及第一个参数意义不同外。还有如下两点:
- 事件处理程序的作用域不相同:addEventListener的作用域是元素本身,this指的是触发元素。而attachEvent事件处理程序会在全局变量内运行,this指的是window,所以刚才的例子返回的结果是undefined,而不是元素id。
- 为一个事件添加多个事件处理程序时,执行顺序不同:使用addEventListener时浏览器会按照添加顺序执行,IE浏览器使用attachEvent时,如果添加的方法过多时,IE浏览器将不会按照顺序执行。
-
阻止事件冒泡与阻止默认事件
阻止事件冒泡 stopPropagation() 方法
可以阻止事件冒泡,也可以阻止事件捕获,也可以阻止处于目标阶段
使用stopPaopagation()方法可以停止事件在DOM层次的传播,不再派发事件。
1 <div id="p">parent 2 <div id="c">child</div> 3 </div> 4 <script type="text/javascript"> 5 var p = document.getElementById('p'), 6 c = document.getElementById('c'); 7 c.addEventListener('click', function (e) { 8 e.stopPropagation() 9 alert('子节点冒泡') //不再向上冒泡到父级 10 }, false); 11 p.addEventListener('click', function () { 12 alert('父节点冒泡')}, false); 13 </script>
阻止默认事件 event.preventDefault() 方法 (基本没作用吧...很少有需求将默认事件取消掉吧)
event.preventDefault()