skip to content
usubeni fantasy logo Usubeni Fantasy

复习 DOM 事件机制

/ 9 分钟阅读

本期前端复习的主题是 DOM 事件机制,是前端开发非常重要的基础。除了常听到的“冒泡”,完整的事件流其实更有意思。

事件传播

完整的 DOM 事件传播分为三个阶段:

  1. 捕获阶段(Capturing Phase)
    • 事件从 window 一路向下传递到目标元素的父节点。
    • 期间可以通过 addEventListener(type, listener, true) 第三个参数设为 true 来监听此阶段。
  2. 目标阶段(Target Phase)
    • 事件到达目标元素本身,即 event.target
    • 此阶段监听函数会被触发。
  3. 冒泡阶段(Bubbling Phase)
    • 事件从目标元素向上传播至 window
    • 默认通过 addEventListener(type, listener, false) 注册的事件监听器会在这个阶段触发。

但也不是所有事件都支持冒泡,例如 focus, blur 等就不冒泡,具体各个事件是否支持冒泡可以 w3c 官方文档

监听事件

<div id="outer" class="box">
Outer
<div id="middle" class="box">
Middle
<div id="inner" class="box">Inner</div>
</div>
</div>

事件监听注册如下:

const boxes = ["outer", "middle", "inner"];
boxes.forEach((id) => {
const el = document.getElementById(id);
// 事件捕获阶段
el.addEventListener(
"click",
(event) => logEvent("捕获阶段", id, event),
true, // 捕获阶段
);
// 事件冒泡阶段
el.addEventListener(
"click",
(event) => logEvent("冒泡阶段", id, event),
false, // 冒泡阶段
);
});

可以参考这个示例:https://codesandbox.io/p/sandbox/w93cgd

阻止传播

调用 event.stopPropagation() 可以阻止事件传播。注意,这不仅能停掉冒泡,在捕获阶段也能把事件截下来。

举个例子:

child.addEventListener("click", (event) => {
event.stopPropagation();
console.log("child");
});

此时点击按钮,只会输出 child,不会触发 parentgrandparent 的监听器。

同理,如果是在捕获阶段调用 stopPropagation(),事件就无法到达目标元素和冒泡阶段:

parent.addEventListener(
"click",
(event) => {
event.stopPropagation();
console.log("parent capture");
},
true,
); // 注意第三个参数 true 开启捕获

此时点击子元素,事件在 parent 被截获,child 的点击事件将永远不会触发

常见场景

  1. 防止重复触发(阻止冒泡)
    • 场景:点击卡片中的按钮(如“删除”),但不希望触发卡片本身的点击事件(如“跳转详情”)。
    • 做法:在按钮的点击事件中调用 event.stopPropagation()
  2. 全局拦截(阻止捕获)
    • 场景:页面进入“编辑模式”或“引导模式”,需要禁用页面上所有元素的点击交互,只允许特定区域或完全接管交互。
    • 做法:在 window 或最外层容器上监听 click 事件(设置 capture: true),并调用 event.stopPropagation(),这样内部元素都无法收到点击事件。

如果同一个元素绑定了多个监听器,想把后续的监听器也一并拦住,得用 event.stopImmediatePropagation()

默认行为

浏览器会对某些事件执行默认动作。例如:

  • 点击 <a> 标签会跳转链接。
  • 点击表单的提交按钮会提交表单。
  • 在输入框按键会输入字符。
  • 选中文本后右键会弹出上下文菜单。

我们可以使用 event.preventDefault() 来阻止这些默认行为。

跟不是所有事件都会冒泡一样,也并非所有事件的默认行为都能被取消。可以通过 event.cancelable 属性查看事件是否可取消。

passive

passive 的意思是“被动”。用这个选项,等于向浏览器承诺:在这个监听器里,我绝不调用 preventDefault()

既然你不打算阻止滚动,浏览器就不用等你的 JS 跑完再动,页面滚动自然就丝滑多了。否则,浏览器为了确认你会不会拦截滚动,每一帧都得等你,很容易卡顿。

现代浏览器为了优化体验,默认把 touchstartwheel 等滚动事件设为 passive: true。也就是说,你在这个监听器里调 preventDefault() 是没用的。如果非要阻止滚动,必须在绑定时显式加上 { passive: false }

// 默认情况下 passive 为 true,preventDefault() 无效
document.addEventListener("touchstart", function (e) {
e.preventDefault(); // 控制台会显示警告,滚动无法阻止
});
// 显式设置 passive: false,preventDefault() 生效
document.addEventListener(
"touchstart",
function (e) {
e.preventDefault(); // 阻止滚动
},
{ passive: false },
);

阻止传播与默认行为的影响

别搞混了:阻止传播(Stop Propagation)阻止默认行为(Prevent Default) 是两码事。

  • stopPropagation():让事件不再通过 DOM 树传播(冒泡/捕获),但阻止浏览器执行默认动作。
  • preventDefault():告诉浏览器不要做默认动作,但阻止事件在 DOM 中的传播。

连锁效应

虽然两者独立,但要注意一个事件的默认行为可能是触发另一个事件

例如,在输入框中按键,keydown 事件的默认行为通常包括“将字符输入到文本框”。如果你在 keydown 阶段调用了 event.preventDefault(),浏览器就会取消这个默认动作,即使你阻止的不是 input 的默认行为,字符也不会出现在输入框中。

示例

有个常见的坑:粗暴地在全局阻止默认行为。例如,为了禁用某个快捷键,但在 document 上直接阻止了所有 keydown 的默认行为:

document.addEventListener("keydown", (e) => {
// 这会导致整个页面的输入框即使获得焦点也无法输入文字
// 因为“输入文字”也是按键的默认行为之一
e.preventDefault();
});

所以在调用 preventDefault() 前,一定要加条件判断(比如只针对特定键码 e.key === 'Enter' 阻止)。

事件委托

这是冒泡最实用的功能。

有了冒泡,**事件委托(Event Delegation)**才成为了可能:

document.getElementById("parent").addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
console.log("Clicked button:", e.target.id);
}
});

这样可以只给 parent 绑定一次事件监听器,而不需要为每个 button 单独绑定,提高性能。

这里就得区分 targetcurrentTarget 了:

  • target 是事件触发的具体目标元素。
  • currentTarget 是事件监听器绑定的当前元素。

不得不吐槽,这个命名属实有点抽象,久了不用就总会把 currentTarget 记成是当前触发的元素,这就反了😂

<div id="parent">
<button id="child">Click me</button>
</div>
<script>
const parent = document.getElementById("parent");
parent.addEventListener("click", function (e) {
console.log("target:", e.target);
console.log("currentTarget:", e.currentTarget);
});
</script>

点击按钮 <button id="child"> 时:

  • e.target<button>:你点的元素
  • e.currentTarget<div>:绑定事件的元素(parent)

总结

  • 传播机制:事件流分为捕获、目标、冒泡三个阶段。日常开发主要利用冒泡进行事件委托,但在特定场景下捕获阶段也可以用于拦截事件。
  • 行为控制:区分 stopPropagationpreventDefault。前者阻止事件继续传播,后者阻止浏览器的默认行为(如表单提交),两者互不影响。
  • 性能优化:滚动类事件(如 touchstart, wheel)建议使用 passive: true,明确告知浏览器不会调用 preventDefault,从而让页面滚动更加流畅。
  • 对象区分event.target 是实际触发事件的元素,event.currentTarget 是绑定监听器的元素。在处理复杂的组件嵌套时,分清这两个属性非常重要。

参考文献

评论组件加载中……