导读:
当我们点击一个元素触发该元素上绑定的点击事件时,同时也点击了该元素的父元素以及祖先元素等元素,如果该元素的父元素以及祖先元素等元素也绑定了点击事件,那么它们的执行顺序是怎样的呢?为了解决这个问题,所以有了事件流的概念。
1、事件流
当一个HTML元素产生一个事件时,该事件会在元素节点与根结点之间的路径传播,路径所经过的结点都会收到该事件,这个传播过程可称为DOM事件流。简单地说:事件流就是描述页面中接收事件的顺序。其包含三个阶段:
- 捕获阶段:事件从 Document 节点自上而下向目标节点传播的阶段;
- 目标阶段:真正的目标节点正在处理事件的阶段;
- 冒泡阶段:事件从目标节点自下而上向 Document 节点传播的阶段。
在整个事件流的任何位置通过调用事件的 stopPropagation()
方法可以停止事件的传播过程。
DOM同时支持两种事件模型:捕获型事件(事件句柄在捕获阶段执行)和冒泡型事件(事件句柄在冒泡阶段执行)。现在主流的是冒泡事件。
冒泡事件
事件冒泡即在某一 DOM 元素被触发时,从该节点开始然后逐级向上触发事件,直至到达文档根。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"/> <title>事件流测试</title> </head> <body> <div id="parent"> <button id="child">Click me</button> </div> <script type="text/javascript"> var child = document.getElementById(‘child‘); var parent = document.getElementById(‘parent‘); // 创建冒泡事件,将 addEventListener 的第三个参数设为 false child.addEventListener(‘click‘,function(){ alert(‘0 - I am child‘); },false); parent.addEventListener(‘click‘,function(){ alert(‘1 - I am parent‘); },false); document.body.addEventListener(‘click‘,function(){ alert(‘2 - I am body‘); },false); document.addEventListener(‘click‘,function(){ alert(‘3 - I am document‘); },false); window.addEventListener(‘click‘,function(){ alert(‘4 - I am window‘); },false); </script> </body> </html>
当我们点击页面中的按钮后会依次弹出:
- 0 - I am child
- 1 - I am parent
- 2 - I am body
- 3 - I am document
- 4 - I am window
另外有一些不支持冒泡的事件:
- focus: 元素获得焦点时触发,不支持冒泡。
- blur:元素失去焦点时触发,不支持冒泡。
- mouseenter:鼠标移入元素触发,不支持冒泡。
- mouseleave:鼠标移出元素时触发,不支持冒泡。
捕获事件
事件捕获的概念,与事件冒泡正好相反。事件从目标元素的所有祖先元素依次往下传递。创建捕获事件,只需要将 addEventListener()
的第三个参数设为 true 即可。再次点击页面中的按钮会依次弹出:
- 4 - I am window
- 3 - I am document
- 2 - I am body
- 1 - I am parent
- 0 - I am child
2、事件委托
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
结合上述事件冒泡这一个机制,那么我们给最外面的元素添加点击事件,当点击里面元素的时候,都会冒泡到最外层的元素上,所以其祖先元素都会触发,这就是事件委托,委托它们先祖代为执行事件。
当然此时点击里面的元素至委托的先祖元素间的所有元素也会执行事件,如果想让事件委托的效果跟直接给节点的事件效果一样,我们需要使用 Event 对象提供的一个属性叫 target
,可以返回触发事件的目标节点,我们称之为事件源,也就是说,target
可以表示为当前的事件操作的 Dom,但是不是真正操作 Dom,当然,这个是有兼容性的,标准浏览器用 ev.target
,IE浏览器用 event.srcElement
,此时只是获取了当前节点的位置,然后我们用 nodeName 来获取具体是什么标签名。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"/> <title>事件委托测试</title> </head> <body> <div id="parent"> <button id="child0">Click me 0</button> <button id="child1">Click me 1</button> <button>Click me 2</button> </div> <script type="text/javascript"> var parent = document.getElementById(‘parent‘); parent.addEventListener(‘click‘,function(ev){ var ev = event || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == ‘button‘){ alert(‘n - I am child‘); } },false); </script> </body> </html>
上面的例子中我们把事件绑定在了按钮的父元素上,但是只有在点击按钮时才会执行事件。不过显然上面操作的是同样的效果,要是每个子元素被点击后执行效果都不一样,那么用事件委托还有用吗?答案是肯定的,我们来看下面的例子:
parent.addEventListener(‘click‘,function(ev){ var ev = event || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == ‘button‘){ switch(target.id) { case ‘child0‘:alert(‘1 - I am child0‘); break; case ‘child1‘:alert(‘2 - I am child1‘); break; default:alert(‘3 - I am child2‘); } } },false);
目前讲的都是在 Document 加载完成后,在现有 Dom 节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?
用事件委托的方式,新添加的后代元素依然是带有事件效果的。我们可以发现,当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在 Js 里面的执行,所以事件委托的方式不仅使得新添加的后代元素带有事件效果,而且这样也可以大大的减少了 Dom 操作。
现在又有一个新的问题,上面的例子在分配不同的函数(事件触发后执行的函数)时,我们需要知道对应子元素的一个独有的属性,但是新增加的元素的属性我们无法预知,所以没办法像上面一样添加指定的函数,这里我们有一个折中的办法,即使不及之前的:
parent.addEventListener(‘click‘,function(ev){ var ev = event || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == ‘button‘){ target.style.backgroundColor="#00FF00"; } },false);
写在最后
事件流和事件委托的介绍在这里就告一段落了,写得可能有些乱,仅供参考吧,也算作是自己的学习笔记,如果文中有什么错误,欢迎大家指正,愿与大家在交流之中共同进步,愈激烈,愈深刻。感谢。