当直接设置为 true|false
的时候,true
则表示使用事件捕获,false
表示使用事件冒泡。而使用 options
形式的时候,capture
设置为 true
表示使用事件捕获,false
表示使用事件冒泡。同时,这两种形式的参数都是可选的,如果省略了第三个参数的话,则会使用 false
来设置默认值,意味着这个事件使用事件冒泡。事件冒泡/捕获相关的可以通过查看 MDN 文档来了解更多。
如果在处理事件的时候选择了事件捕获,那意味着需要知道事件的起源以及它是如何传播的。
所有的事件都是从windows开始,并且首先经过捕获阶段。当事件被绑定时,会从windows上开始逐级向DOM树下传递事件,直到抵达目标元素节点。如果设置的是事件冒泡,则也会发生这种情况。查看以下示例代码:
当用于点击 #c
的时候,将会分派一个源自于 window
对象的事件,然后这个事件会通过它的子级传播,如下所示:
window
→ document
→ <html>
→ body
→ #a
→ #b
→ #c
,以此类推,直到到达目标元素节点。即便没有在 window
、document
、<html>
或者是 <body>
上监听事件也没关系,事件的起源依旧是从 window
上发出,并向上描述得那样开始传播。在例子中,点击元素事件就会开始传播。这意味着点击事件开始时,浏览器会询问 window
以下几个问题:
“window
在捕获阶段有什么东西在监听点击事件吗?“,如果有则会触发对应的处理事件,没有则什么都不会发生。
事件现在传播到 document
了,浏览器会问:“document
在捕获阶段有什么东西在监听点击事件吗?”,如果有则触发,没有则什么也不做。
事件现在到达了 <html>
,浏览器问:“ <html>
在捕获阶段有什么东西在监听点击事件吗?”,如果有则触发,没有则什么也不做。同样的步骤还会在 #a
和 #b
上重复发生,直到到达元素节点 #c
。浏览器会问:“#c
在捕获节点有什么东西在监听点击事件吗?”,这次它得到的回答是肯定的:“Yes!!!” 事件到达目标的这段时间被称之为“目标阶段”。此时,事件处理程序将触发,浏览器将 console.log('#c 被点击了')
。事件到这里就结束了?不,不是的,这个过程根本没有完成。过程还在继续,它的路还要继续走下去,但是它现在要走的路是“返航”(事件冒泡阶段)。整个行为就像是蝙蝠的超声波反弹的过程。
程序执行到这以后,浏览器会问:“#c
在冒泡阶段有什么东西在监听点击事件吗?”,“Yes!!!”,事件触发,但需要注意的是,这么做是可能的,事件监听在点击(或者是其他事件类型)都存在 捕获 和 冒泡 阶段。如果在两个阶段都绑定了事件处理程序(比如设置了两次 addEventListener
,一次为 capture: true
,一次为 capture: false
)的话,那么事件就会一起执行,只是他们执行的阶段不同(一次在捕获,一次在冒泡)。
接下来事件传播的方向将会和捕获相反,改为由下而上传递,#c
已经执行事件完毕了,接下来浏览器会问 #b
:“#b
在冒泡阶段有什么东西在监听点击事件吗?”,有则触发事件,没有则什么也不做。就这样事件会逐级向上询问,直到 window
,“window
上有什么东西在监听点击事件吗?”,“没有!快滚,再问我打死你!!!”(抖个机灵~)。这是一段非常漫长的过程,从这个过程中也能想通为什么页面元素一多,嵌套层级很深的时候,页面就会慢的卡顿、响应慢了。
那现在不妨动手试试?
点击元素以后,控制台将输入:
你可以在这里点击试试,点击文字后打开控制台查看输出
了解过事件是如何捕获和冒泡了以后,我们可以看向 stopPropagation
了。
stopPropagation()
可以在(大部分)本地DOM事件上调用该方法,为什么要说大部分?因为像 focus
blur
load
scroll
等一类的事件不属于这一类。但依旧可以调用 event.stopPropagation()
方法,只是它们什么也不会发生,因为事件根本不会传播。
它所做的事情则是当事件到达该方法内部以后,阻止事件继续向下传递,会从当前位置截断。这个时候再去点击 #c
则会打印出如下:
能发现就连冒泡阶段的事件都被阻止了,因为事件在进行到 #b
捕获阶段时,就已经被停止了事件传播,链条在此断开了所以不会继续向下执行。
这是个什么方法?奇奇怪怪的。它类似于 stopPropagation
,但作用不是阻止事件传播到后代(捕获)或祖先(冒泡),此方法进适用于将多个事件处理程序绑定到单个元素的时候。由于 addEventListener()
支持多种事件类型,所以完全可以将多个事件处理函数绑定到单个元素。发生这种情况的时候,(大多数浏览器)事件处理程序按照他们绑定的顺序执行,调用 stopImmediatePropagation
可以防止其后续事件的执行,看看实例:
这么做的话,点击元素以后,控制台会输出:
因为第二个事件处理函数中调用了 ev.stopImmediatePropagation()
方法,所以第三个事件处理程序永远不会运行,但如果改为 ev.stopPropagation()
,第三个方法将会继续向下执行。
stopPropagation()
是阻止“向下”(捕获)或“向上”(冒泡)传播的方法,那 preventDefault()
方法是做什么的?好像它也做了一样的事?但其实不是的,他俩经常被混淆,实际上他们彼此之间并没有太大关系。当你看到 preventDefault()
的时候,你需要想的是 “什么行为” 这个词,然后联想到“阻止默认行为(操作)”。
问题由此而来,默认操作是什么?嗯。。。这个问题其实很难回答,因为它的执行是依赖这篇文章所讨论的 Element + Event 的组合。更让人满脸问号的是,有时候根本没有默认操作!举个简单例子:
点击以上链接的时候会发生什么?是的,跳转链接。在这种情况下,元素是一个锚点,事件是一个点击事件。<a>
+ click
的时候,页面将会跳转至链接,如果我不想跳转的话怎么办?那就需要 preventDefault()
来帮助你了。
yep!确实阻止了事件的传递。在以上的例子中,浏览器页面不会跳转,只会在控制台打印一段 我触发了点击,但并不像跳转
文字。响应的默认事件有很多,比如说 <form>
元素的 submit
事件,我们可以利用preventDefault()
来阻止表单的默认提交事件。又或者是说 <input>
元素的 keypress
(或者是 keydown
| keyup
)事件,我们可以当输入字符达到一定上限时阻止用户继续输入。
如果在一个事件中,同时调用以上三个方法时将会发生什么?
以上代码将会使得当前页面毫无作用,你什么也做不了。因为所有的事件起源于 window
,所以所有的 click
、keydown
、 mousedown
、 contextmenu
和 mousewheel
和其他元素的事件监听和任何程序都将是死路一条。除此之外,还调用了 stopImmediatePropagation()
,后续在引入这个代码片段的页面也将会变得毫无响应。
有一点需要注意的是,stopPropagation()
和 stopImmediatePropagation()
并不是使得页面变得无效的原因,它们做的事情只是阻止事件到达原本的目标而已。同时还调用了 preventDefault()
这才是使整个页面无效的真正原因。