事件传播(事件捕获与事件冒泡)
EventTarget接口
DOM 的事件操作(监听和触发),都定义在EventTarget
接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequest
、AudioNode
、AudioContext
)也部署了这个接口。
该接口主要提供三个实例方法。
addEventListener
:绑定事件的监听函数removeEventListener
:移除事件的监听函数dispatchEvent
:触发事件
事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。
事件冒泡
微软提出了名为事件冒泡(event bubbling)
的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。
因此在事件冒泡的概念下在p元素上发生click事件的顺序应该是
p -> div -> body -> html -> document
事件捕获
网景提出另一种事件流名为事件捕获(event capturing)
。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。
因此在事件捕获的概念下在p元素上发生click事件的顺序应该是
document -> html -> body -> div -> p
在微软和网景打得火热之时,w3c 采用折中的方式,制定了统一的标准——先捕获再冒泡。
addEventListener的第三个参数就是为冒泡和捕获准备的.
addEventListener有三个参数:
element.addEventListener(event, function, useCapture)
第一个参数是需要绑定的事件
第二个参数是触发事件后要执行的函数
第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。
注意,第三个参数useCapture除了布尔值,还可以是一个属性配置对象。该对象有以下属性。
capture
:布尔值,表示该事件是否在捕获阶段
触发监听函数。once
:布尔值,表示监听函数是否只触发一次,然后就自动移除。passive
:布尔值,表示监听函数不会调用事件的preventDefault
方法。如果监听函数调用了,浏览器将忽略这个要求,并在监控台输出一行警告。
事件捕获vs事件冒泡
当事件捕获和事件冒泡一起存在的情况,事件又是如何触发呢。
这里把被点击的DOM节点为target节点
-
document 往 target节点,捕获前进,遇到注册的捕获事件立即触发执行
-
到达target节点,触发事件(对于target节点上,是先捕获还是先冒泡则按照捕获事件和冒泡事件的注册顺序,先注册先执行)
-
target节点 往 document 方向,冒泡前进,遇到注册的冒泡事件立即触发
总结下就是:
-
对于非target节点则先执行捕获在执行冒泡
-
对于target节点则是先执行先注册的事件,无论冒泡还是捕获
冒泡还是捕获?
对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分,但是由于事件冒泡的事件流模型被所有主流的浏览器兼容,从兼容性角度来说还是建议大家使用事件冒泡模型。
IE浏览器兼容
IE浏览器对addEventListener兼容性并不算太好,只有IE9以上可以使用。
要兼容旧版本的IE浏览器,可以使用IE的attachEvent函数
object.attachEvent(event, function)
两个参数与addEventListener相似,分别是事件和处理函数,默认是事件冒泡阶段调用处理函数,要注意的是,写事件名时候要加上"on"前缀("onload"、"onclick"等)。
封装函数
1 //添加事件
2 function addEvent(obj,type,fn){
3 if(obj.addEventListener){
4 obj.addEventListener(type,fn,false);
5 }else{
6 obj.attachEvent("on"+type,fn);
7 }
8 }
9 //移除事件
10 function romeveEvent(obj,type,fn){
11 if(obj.removeEventListener){
12 obj.removeEventListener(type,fn)
13 }else{
14 obj.detachEvent("on"+type,fn);
15 }
16 }
事件冒泡应用:事件代理(事件委托)
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
在实际的开发当中,利用事件流的特性,我们可以使用一种叫做事件代理(事件委托)的方法。
使用事件代理的好处不仅在于将多个事件处理函数减为一个,而且对于不同的元素可以有不同的处理方法。
var oUl=document.querySelector("ul"); oUl.onclick=function(e){ var evt = e || event; if(evt.nodeName.toLowerCase=="li"){ alert("li"); } }
接口的target
属性是对Event
调度事件的对象的引用。它与Event.currentTarget
在事件的冒泡或捕获阶段调用事件处理程序时不同。
event.target
可以使用该属性来实现事件代理。
在IE 6-8上,事件模型是不同的。事件侦听器使用非标准EventTarget.attachEvent()
方法附加。在此模型中,事件对象具有Event.srcElement
属性而不是target
属性,并且它具有与之相同的语义Event.target
。
function hide(e) { // 兼容处理 var evt=e || event; var target = evt.target || evt.srcElement; target.style.visibility = 'hidden'; }
如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation
方法(IE为cancelBubble)。
if(evt.stopPropagation){ evt.stopPropagation(); }else{ evt.cancelBubble = true; }
但是,stopPropagation
方法只会阻止事件的传播,不会阻止该事件触发<p>
节点的其他click
事件的监听函数。也就是说,不是彻底取消click
事件。
p.addEventListener('click', function (event) { event.stopPropagation();
//会触发 console.log(1); }); p.addEventListener('click', function(event) { // 会触发 console.log(2); });
上面代码中,p
元素绑定了两个click
事件的监听函数。stopPropagation
方法只能阻止这个事件向其他元素传播。因此,第二个监听函数会触发。输出结果会先是1,然后是2。
如果想要彻底阻止这个事件的传播,不再触发后面所有click
的监听函数,可以使用stopImmediatePropagation
方法。
p.addEventListener('click', function (event) { event.stopImmediatePropagation(); // 会触发 console.log(1); }); p.addEventListener('click', function(event) { // 不会被触发 console.log(2); });
上面代码中,stopImmediatePropagation
方法可以彻底阻止这个事件传播,使得后面绑定的所有click
监听函数都不再触发。所以,只会输出1,不会输出2。
阮一峰老师网站上有着详尽的解释,如有需求,敬请关注
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步