前言
JavaScript 事件流描述的是从页面中接收事件的顺序,这个流程从最外层的对象开始(通常是 window
对象),然后经过特定的元素,直到达到触发事件的目标元素。理解事件流对于理解事件处理机制和实现事件委托是非常重要的。
正文
事件机制是指当特定的操作(如点击按钮、移动鼠标等)在DOM元素上发生时,会触发相应的事件。JavaScript通过监听事件并绑定对应的处理函数来响应用户的操作,对用户的交互做出响应。
事件流有三个阶段:
1. 捕获阶段(Capture Phase)
事件从最外层的对象(通常是 window
对象)开始,然后向下穿过DOM树,直到达到触发事件的目标元素。在捕获阶段,事件是从外向内传播的。
例如:window -> document -> html -> body -> div。
2. 目标阶段(Target Phase)
事件已经到达了触发它的目标元素。这是事件处理函数实际执行的阶段。
3. 冒泡阶段(Bubbling Phase)
事件从目标元素开始向上冒泡,经过DOM树上的父元素,直到达到最外层的对象(通常是 window
对象)。在冒泡阶段,事件是从内向外传播的。
例如:div -> body -> html -> document -> window。
这是我从网上找来的一张图,相信大家看完之后可以更好的理解事件捕获和事件冒泡。
好的,相信大家看完事件流的概念之后,对事件流这个机制有一定的理解,接下来我们来看看事件捕获和事件冒泡的例子。这里我们使用addEventListener
方法来绑定捕获和冒泡事件。
事件绑定
element.addEventListener(event, handler, useCapture);
addEventListener有三个参数,我们来介绍一下:
1. event(事件类型)
这是一个字符串,指定要监听的事件类型,如 "click"
, "mouseover"
, "keydown"
等。
2. handler(事件处理函数)
这是一个函数,当指定的事件类型在指定的元素上触发时,该函数就会被调用。事件对象(Event
对象)将作为参数传递给处理函数,你可以使用这个对象来获取事件的详细信息,如事件目标、鼠标坐标、键盘按键等。
3. useCapture(是否使用捕获阶段)
这是一个可选的布尔值,决定事件是在捕获阶段(true
)还是冒泡阶段(false
)进行处理。
true
:使用捕获阶段进行事件处理。false
:使用冒泡阶段进行事件处理。
默认值:如果不提供 useCapture
参数,它默认为 false
,即在冒泡阶段处理事件。
<style> #app{ width: 400px; height: 400px; background-color: aqua; } #wrap{ width: 200px; height: 200px; background-color: blueviolet; } #box{ width: 100px; height: 100px; background-color: #000; } </style> <div id="app"> <div id="wrap"> <div id="box"></div> </div> </div> <script> let app = document.getElementById('app') let wrap = document.getElementById('wrap') let box = document.getElementById('box') app.addEventListener('click', () => { console.log('app'); }, true) wrap.addEventListener('click', () => { console.log('wrap'); }) box.addEventListener('click', () => { console.log('box'); }) </script>
我们点击图中黑色方块,也就是box
容器,然后输出box, wrap, app,因为我们的addEventListener
函数第三个并没有写参数,而它在没有参数的情况下默认是false,也就是说该事件会在冒泡阶段时处理。我们上面提到过,事件从目标元素开始向上冒泡,经过DOM树上的父元素,直到达到最外层的对象,而因为box被包裹在wrap里,wrap被包裹在app里,所以是先输出box,wrap,app.
而如果点击紫色元素,则输出wrap, app。点击蓝色,输出app。
app.addEventListener('click', () => { console.log('app'); }, true) wrap.addEventListener('click', () => { console.log('wrap'); }) box.addEventListener('click', () => { console.log('box'); },true)
这里我们将app
和box
容器点击事件的第三个参数设置为true
,也就是说两个容器的点击事件会在捕获阶段时处理。而wrap
的点击事件在冒泡阶段处理。那么我们点击box
(黑色容器),将输出app, box, wrap。当我们点击box
容器时,先进行捕获阶段,事件从最外层元素开始,逐渐向内部元素传播,所以先打印app,再打印box。当事件到达目标元素时,再发生冒泡事件,从目标元素开始,逐渐向外部元素传播,所以最后打印wrap。
而如果我们将三个点击事件的第三个参数全部设置为true时,那么他们都将在捕获阶段进行处理,则打印app, wrap, box。
阻止事件传播
event.stopPropagation()
当我们调用此方法时,会阻止事件进行传播下去,但不会阻止其它事件处理程序被触发。我通过一个例子来给大家讲解:
app.addEventListener('click', (e) => { // 绑定, 订阅, 注册 console.log('app'); e.stopPropagation() }, true) wrap.addEventListener('click', () => { console.log('wrap'); }, ) box.addEventListener('click', (e) => { console.log('box'); }, true)
如上述代码,在正常情况下,点击box
容器,如果不加这一行代码e.stopPropagation()
, 那么app
和box
在捕获阶段触发,wrap
在冒泡阶段触发。但如果在app
容器的事件监听函数上面加了这一段代码,那么它会阻止事件进行传播。也就是说,当捕获阶段时,事件传播到了app
的容器上,发现addEventListener
函数的第三个参数为true,那么则触发该点击事件,但是发现这个点击事件的回调函数内有e.stopPropagation()
,它会阻止事件继续传播下去,所以本该继续向内传播的事件被终止。所以这里只打印app
。
app.addEventListener('click', (e) => { // 绑定, 订阅, 注册 console.log('app'); }, true) wrap.addEventListener('click', () => { console.log('wrap'); }, ) box.addEventListener('click', (e) => { console.log('box'); e.stopPropagation() })
来看看这段代码,点击box
容器,那么在捕获阶段打印app
,在冒泡阶段,当事件传播到box
身上时,触发,打印box
,但是监听器的回调函数里面存在e.stopPropagation()
,所以它会阻止事件继续传播,最终打印 app, box。
调用该方法,不会阻止同一元素其它事件被触发
app.addEventListener('click', (e) => { // 绑定, 订阅, 注册 console.log('app'); }, true) wrap.addEventListener('click', () => { console.log('wrap'); }, ) box.addEventListener('click', (e) => { console.log('box'); e.stopPropagation() }) box.addEventListener('click', (e) => { console.log('box2'); })
如图,打印app, box, box2
。该方法不会阻止同一元素其他事件被触发。
event.stopImmediatePropagation()
调用该方法,它的效果跟event.stopPropagation()类似,但是会阻止同一元素的相同事件被触发
app.addEventListener('click', (e) => { // 绑定, 订阅, 注册 console.log('app'); }, true) wrap.addEventListener('click', () => { console.log('wrap'); }, ) box.addEventListener('click', (e) => { console.log('box'); event.stopImmediatePropagation() }) box.addEventListener('click', (e) => { console.log('box2'); })
若我们在box
的监听上加上event.stopImmediatePropagation()
函数,那么它会阻止box
元素相同事件的触发,例如上述代码中box
绑定的第二段点击事件。所以只会输出app, box
。
事件委托
事件委托(Event Delegation)是一种常用的 JavaScript 设计模式,用于处理事件监听和处理,特别是当需要为大量的子元素添加相同类型的事件监听器时。通过事件委托,我们可以将事件监听器绑定到父元素(通常是包含所有子元素的容器),而不是直接绑定到每一个子元素。
<ul id="ul"> <li>a</li> <li>b</li> <li>c</li> <li>d</li> <li>e</li> </ul>
如果我们想为每一个列表项添加点击事件,传统的方法可能是遍历每一个列表项并分别绑定事件处理函数:
let lis = document.querySelectorAll('li') lisforEach((li) => { li.addEventListener('click', () => { console.log(li.innerText); }) }) 虽然这样添加点
击事件也是没问题的,但是这只是节点少的情况下,如果节点多的时候,那么就需要造成十分大的性能消耗。
使用事件委托,我们只需将事件监听器绑定到 ul
元素:
let ul = document.getElementById('ul') ul.addEveListener('click', (e) => { // console.log(e); console.log(e.target.innerText); })
当你点击列表项时,事件会冒泡到ul
元素,然后通过检查e.target.innerText
,来判断我们点击的是列表项中的哪一个元素。
不管li
有多少个,我们最终只需要维护一个函数就够了!
总结一下
注意事项:
- 事件冒泡:事件委托依赖于事件冒泡机制,因此确保不阻止事件冒泡或在冒泡阶段处理事件。
- 目标检查:在事件处理函数中,通常需要检查
event.target
或event.currentTarget
来确定是哪一个元素触发了事件。 - 选择合适的父元素:选择一个恰当的父元素进行事件委托,通常是包含所有目标元素的最近的父容器。
事件委托是一种强大而灵活的技术,特别适用于处理大量的子元素事件。通过将事件监听器绑定到父元素,我们可以提高性能,简化代码,并支持动态添加的元素,从而更高效地管理和处理事件。