4.深入 addEventListener

查看飞书原文 ↗2,931 字符
​
我们最常用的 element.addEventListener('click', handler) 写法,其实只用到了它的前两个参数。addEventListener 的第三个参数,可以极大地改变事件监听的行为,主要涉及两个关键概念:事件的“捕获”阶段和滚动性能的“被动”优化。​
​
1. 事件的完整生命周期:捕获与冒泡​
​
当一个事件发生时,它在 DOM 中的传播分为三个阶段:​
​
1.
捕获阶段 (Capturing Phase): 事件从最外层的祖先元素(window)开始,逐级“向下”传播,直到达到真正的目标元素。​
2.
目标阶段 (Target Phase): 事件到达目标元素。​
3.
冒泡阶段 (Bubbling Phase): 事件从目标元素开始,逐级“向上”冒泡,直到再次回到 window。​
​
默认情况下,所有事件监听器都在“冒泡阶段”执行。​
2. useCapture: 在捕获阶段监听​
​
addEventListener 的第三个参数,如果传入一个布尔值 true,或者一个包含 capture: true 的对象,就可以让事件监听器在捕获阶段被触发。​
​
•
语法:​
// 传统方式
element.addEventListener('click', handler, true);

// 现代方式(推荐)
element.addEventListener('click', handler, { capture: true });
•
作用: 允许你比目标元素更早地、在事件“下沉”的过程中就拦截到它。​
•
核心场景: 通常用于需要在事件到达最终目标前,就进行统一拦截处理的场景。虽然不常用,但在框架或大型应用的顶层事件分析、日志记录或阻止某些行为时可能有用。​
​
示例:观察捕获与冒泡的顺序​
​
HTML 结构:​
<div id="parent">
  Parent
  <p id="child">Child</p>
</div>
​
JavaScript 实现:​
const parent = document.getElementById('parent');
const child = document.getElementById('child');

// 在 Parent 上设置一个捕获阶段的监听器
parent.addEventListener('click', () => {
  console.log('1. Parent - Capturing');
}, { capture: true }); // 或直接用 true

// 在 Parent 上设置一个冒泡阶段的监听器(默认)
parent.addEventListener('click', () => {
  console.log('4. Parent - Bubbling');
}, { capture: false }); // 或不写第三个参数

// 在 Child 上设置监听器(目标阶段)
child.addEventListener('click', () => {
  console.log('2. Child - Target/Bubbling Start');
  // 注意:目标阶段的触发顺序遵循代码注册顺序
});

child.addEventListener('click', () => {
    console.log('3. Child - Also on Target');
});
​
运行效果:​
​
当你点击 Child 元素时,控制台的输出顺序将是:​
​
1. Parent - Capturing
2. Child - Target/Bubbling Start
3. Child - Also on Target
4. Parent - Bubbling
​
这个顺序清晰地展示了事件首先从 Parent 向下“捕获”,到达 Child 目标后,再从 Child 向上“冒泡”回 Parent 的完整过程。​
​
3. passive: true: 提升滚动性能的利器​
​
•
问题背景: 对于 touchmove (触摸滚动) 和 wheel (滚轮滚动) 这类事件,浏览器在执行滚动操作前,会先等待你的 JavaScript 监听器执行完毕。这是因为浏览器不确定你的代码里是否会调用 event.preventDefault() 来阻止滚动。这种等待,尤其在移动端或复杂页面上,会造成明显的滚动卡顿(Jank)。​
​
•
解决方案: 通过设置 { passive: true },你等于是在向浏览器作出一个承诺:“我保证,这个监听器绝对不会调用 event.preventDefault() 来阻止滚动。”​
​
•
作用: 得到这个承诺后,浏览器就可以放心大胆地立即执行滚动,无需等待你的 JS 代码。它会在一个独立的线程里处理滚动,同时在主线程上执行你的监听器函数。这大大提升了滚动的流畅度。​
​
•
语法:​
// 错误的做法(可能导致卡顿)
document.addEventListener('touchmove', () => {
  // 执行一些复杂的计算...
});

// 正确的性能优化做法
document.addEventListener('touchmove', () => {
  // 执行一些复杂的计算...
}, { passive: true });
•
注意事项:​
◦
这是一个重要的性能优化点,特别是对于移动 Web 应用。​
◦
如果你在 { passive: true } 的监听器中尝试调用 event.preventDefault(),该调用将会被浏览器忽略,并且通常会在控制台收到一条警告。​
◦
现代浏览器非常智能,对于 window, document 或 document.body 上的 touchstart 和 touchmove 事件,它们已经默认将 passive 设为 true。​
​
4.
总结​
​
addEventListener 的第三个参数(选项对象)为我们提供了更深层次的控制权。​
​
​
​
选项​
​
​
作用​
​
​
目的​
​
​
capture: true​
​
​
在事件的捕获阶段触发监听器。​
​
​
控制执行时机:更早地拦截事件。​
​
​
passive: true​
​
​
告知浏览器监听器不会阻止默认行为。​
​
​
性能优化:实现流畅的页面滚动。​
​
​
once: true​
​
​
监听器在触发一次后自动移除。​
​
​
代码简洁:避免手动调用 removeEventListener。​
​
​
​
通过组合使用这些选项,可以编写出更高效、更健壮的事件处理代码。​
element.addEventListener('scroll', handleScroll, { passive: true, once: true });​
​