事件传播
1. 网景与微软的早期分歧
在 1990 年代的“浏览器战争”期间,网景和微软各自提出了不同的事件传播模型:
- 网景:事件捕获
网景的浏览器最初采用事件捕获机制:事件从最外层父元素向目标元素逐层传递(即自上而下),开发者可以通过在事件监听中显式启用捕获来处理事件。 - 微软:事件冒泡
微软的 IE 浏览器则采用了事件冒泡机制:事件从目标元素向上逐层传递到父元素(即自下而上)。这种模型更符合直觉,开发者无需额外配置即可监听父元素的事件。
2. W3C 的标准化:融合两种模型
为了解决浏览器兼容性问题,W3C 在 DOM Level 2 Events 规范中统一了两种机制,将事件传播分为三个阶段:
- 捕获阶段 :事件从根节点向下传递到目标元素。
- 目标阶段 :事件到达目标元素。
- 冒泡阶段 :事件从目标元素向上冒泡到根节点。
具体说明
可以通过 addEventListener 的第三个参数选择监听阶段:
true:在捕获阶段处理事件。false(默认值):在冒泡阶段处理事件。
// 示例:捕获阶段监听
element.addEventListener("click", handler, true);
// 示例:冒泡阶段监听(默认)
element.addEventListener("click", handler, false);
案例
1.捕获阶段
<body>
<div class="grandparent">
<div class="parent">
<div class="child">
</div>
</div>
</div>
</body>
<script>
const grandparent = document.querySelector('.grandparent');
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');
//`e.eventPhase`: 表示当前阶段(1=捕获,2=目标,3=冒泡)。
grandparent.addEventListener('click', (e) => {
console.log('红色',e.eventPhase);
},true);
parent.addEventListener('click', (e) => {
console.log('绿色', e.eventPhase);
},true)
child.addEventListener('click', (e) => {
console.log('蓝色', e.eventPhase);
},true)
</script>
事件从根节点(document)开始,自上而下向目标元素传递:
- 触发
grandparent的捕获监听器。 - 触发
parent的捕获监听器。 - 触发
child的捕获监听器(目标元素)。- 无论是注册为捕获阶段还是冒泡阶段的事件监听器,目标元素上的事件处理程序都会在目标阶段执行。
2.冒泡阶段
事件从目标元素 自下而上 冒泡到根节点:
- 触发
child的冒泡监听器。- 目标元素上的事件处理程序都会在目标阶段执行
- 触发
parent的冒泡监听器。 - 触发
grandparent的冒泡监听器。
当同时存在冒泡阶段执行和捕获阶段执行时,记住一个原则:捕获阶段先执行,剩下的冒泡就按照其顺序执行
3.阻止事件传播
1. event.stopPropagation()
-
作用:阻止事件继续向上或向下传播,但不会影响当前元素的其他监听器。
-
用法: element.addEventListener('click', function(event) { event.stopPropagation(); // 其他逻辑 });
2.
event.stopImmediatePropagation()
-
作用:不仅阻止事件传播,还会阻止当前元素上的其他监听器执行。
-
用法:
element.addEventListener('click', function(event) { event.stopImmediatePropagation(); // 此监听器之后的监听器不会执行 });
3. return false(仅在 HTML 属性中有效)
- HTML 属性: 按钮
4.事件代理/委托
事件委托是一种利用事件冒泡机制来处理动态添加元素的事件绑定的技术。它通过在父级元素上设置事件监听器,然后根据事件的目标(即实际触发事件的子元素)来决定如何处理事件。
使用场景
- 动态添加或删除子元素:当页面中的某些元素是动态生成的(例如通过AJAX加载或用户交互生成),并且这些元素需要绑定事件处理程序时。
- 大量相似元素:如果有大量的相似元素需要相同的事件处理逻辑,为每个元素单独绑定事件处理器会消耗较多内存资源。
- 提高性能:减少直接绑定到DOM节点上的事件监听器数量可以显著提高页面性能,尤其是在存在大量需要监听事件的元素时。
优点
- 减少内存占用:只需要一个事件监听器就可以处理多个子元素的事件,而不是为每个子元素都添加一个监听器。
- 简化代码管理:不需要对新添加的元素重新绑定事件,简化了代码维护。
- 提高效率:对于大量子元素的情况,事件委托可以提供更好的性能表现。
我们只在一个地方(即
<ul>元素)绑定了一个点击事件监听器。无论何时向<ul>中动态添加新的<li>元素,都不需要再为它们单独绑定事件监听器,因为事件冒泡机制会确保所有点击事件都会被<ul>上的监听器捕获,并且可以根据event.target属性判断哪个具体的子元素被点击了。
这种做法不仅提高了代码的可维护性,还减少了内存的使用,特别是在处理大量动态元素的情况下显得尤为重要。
补充
本文稍微提了一下DOM Level 2 Events,在JS中事件处理机制可以分为不同的级别,主要包括 DOM0 级事件、DOM2 级事件和 DOM3 级事件,而DOM Level 1规范并没有定义任何与事件相关的接口或方法。这里主播就顺便聊一下:
- DOM0 级事件
- 语法简单:直接将一个函数赋值给 HTML 元素的事件处理属性(如
onclick、onmouseover等)。 - 事件绑定在冒泡阶段:只能在事件冒泡阶段触发事件处理程序。
- 每个元素每个事件类型只能绑定一个处理函数:如果多次为同一个事件属性赋值,后面的赋值会覆盖前面的赋值。
- DOM2 级事件
- 使用
addEventListener方法:通过addEventListener方法为元素绑定事件处理程序,语法为element.addEventListener(eventType, callback, useCapture)。 - 支持事件捕获和冒泡阶段:
useCapture参数为true时,事件在捕获阶段触发;为false或省略时,事件在冒泡阶段触发。 - 可以为同一个元素的同一个事件类型绑定多个处理函数:多个处理函数会按照绑定的顺序依次执行。
- DOM3 级事件
- 继承自 DOM2 级事件:在 DOM2 级事件的基础上进行了扩展,支持更多的事件类型。
- 新增了一些自定义事件类型:如
keyup、keydown、input等,并且可以自定义事件。 - 事件处理机制与 DOM2 级事件相同:同样使用
addEventListener方法,支持事件捕获和冒泡阶段。