哪些事件不会冒泡的事件,以及为什么不支持冒泡?

#dom #bom #R1

目录

1. 事件传播的三个阶段

事件传播分为三个阶段:

// 事件传播示意
/*
1. 捕获阶段(Capturing Phase):从顶层元素向下传播
2. 目标阶段(Target Phase):到达目标元素
3. 冒泡阶段(Bubbling Phase):从目标元素向上传播
*/

document.addEventListener('click', function(e) {
    console.log('捕获阶段');
}, true); // 第三个参数 true 表示在捕获阶段处理

element.addEventListener('click', function(e) {
    console.log('目标阶段');
});

document.addEventListener('click', function(e) {
    console.log('冒泡阶段');
}, false); // 默认是冒泡阶段

2. 不冒泡的事件列表

以下事件不会冒泡:

  • a) 焦点事件:
    • focus
      • 如果 focus 冒泡,当输入框获得焦点时,所有父元素都会收到通知
      • ==额外的性能开销==
    • blur
  • b) 资源事件: → ==加载完了,没必要再冒泡了==
    • load
    • unload
    • abort
    • error
  • c) 鼠标事件: ==enter 和 leave== → 逻辑合理性,不然矛盾了
    • mouseenter
    • mouseleave
  • d) 媒体事件:
    • pause
    • play
    • playing
    • ended
    • volumechange
    • stalled

这些事件只在目标元素上触发

示例代码:

// 焦点事件示例
document.querySelector('input').addEventListener('focus', function(e) {
    console.log('Input focused');
    console.log('这个事件不会冒泡到父元素');
});

// mouseenter/mouseleave vs mouseover/mouseout
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');

// mouseenter 不冒泡
parent.addEventListener('mouseenter', () => {
    console.log('Parent mouseenter'); // 只在真正进入父元素时触发一次
});

// mouseover 会冒泡
parent.addEventListener('mouseover', () => {
    console.log('Parent mouseover'); // 进入子元素时也会触发
});

3. 为什么某些事件不冒泡?

主要有以下几个原因:

3.1. 性能考虑

// 想象如果 focus 事件冒泡会发生什么
<div>
    <div>
        <input id="myInput" />
    </div>
</div>

// 如果 focus 冒泡,当输入框获得焦点时,所有父元素都会收到通知
// 这会导致不必要的性能开销

3.2. 逻辑合理性

// 以 mouseenter 为例
<div class="parent">
    <div class="child"></div>
</div>

// mouseenter 不冒泡是合理的,因为:
// 1. 从 parent 进入 child 时,鼠标实际上并没有"进入"父元素
// 2. 如果冒泡,会导致父元素重复触发 mouseenter 事件

3.3. 事件本身的特性

// 例如 load 事件
<img src="example.jpg" onload="handleLoad()">

// load 事件表示资源加载完成
// 这是一个一次性的状态变化,没有必要向上冒泡

4. 如何处理不冒泡的事件

4.1. 使用捕获阶段

// 如果确实需要在父元素捕获子元素的 focus 事件
parent.addEventListener('focus', function(e) {
    console.log('Focus captured');
}, true); // 使用捕获阶段

4.2. 使用替代事件

// 使用 focusin/focusout 代替 focus/blur
// focusin/focusout 是会冒泡的
element.addEventListener('focusin', function(e) {
    console.log('Focus detected');
});

4.3. 事件委托的替代方案

4.3.1. 对于不冒泡的事件,可以使用 MutationObserver 来监听

// 对于不冒泡的事件,可以使用 MutationObserver
const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        if (mutation.type === 'attributes' && mutation.attributeName === 'focused') {
            // 处理焦点变化
        }
    });
});

observer.observe(element, {
    attributes: true,
    attributeFilter: ['focused']
});

5. 最佳实践

5.1. 选择合适的事件

// 不好的实践
document.addEventListener('mouseenter', function(e) {
    // 试图通过事件委托处理子元素的 mouseenter
}, false);

// 好的实践
document.addEventListener('mouseover', function(e) {
    // 使用冒泡事件实现类似功能
}, false);

5.2. 使用事件委托时注意

// 正确的事件委托
document.addEventListener('click', function(e) {
    if (e.target.matches('.button')) {
        // 处理按钮点击
    }
});

// 对于不冒泡的事件,直接绑定到目标元素
document.querySelectorAll('input').forEach(input => {
    input.addEventListener('focus', handleFocus);
});

6. 检测事件是否冒泡

// 检测事件是否冒泡
function isEventBubbling(eventName) {
    const element = document.createElement('div');
    let bubbles = false;
    
    element.addEventListener(eventName, function(e) {
        bubbles = e.bubbles;
    });
    
    // 触发事件
    const event = new Event(eventName, {
        bubbles: true,
        cancelable: true
    });
    element.dispatchEvent(event);
    
    return bubbles;
}

// 使用示例
console.log(isEventBubbling('click')); // true
console.log(isEventBubbling('focus')); // false