本期前端复习的主题是 DOM 事件机制,是前端开发非常重要的基础。除了常听到的“冒泡”,完整的事件流其实更有意思。
完整的 DOM 事件传播分为三个阶段:
window 一路向下传递到目标元素的父节点。addEventListener(type, listener, true) 第三个参数设为 true 来监听此阶段。event.target。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,不会触发 parent 或 grandparent 的监听器。
同理,如果是在捕获阶段调用 stopPropagation(),事件就无法到达目标元素和冒泡阶段:
parent.addEventListener(
"click",
(event) => {
event.stopPropagation();
console.log("parent capture");
},
true,
); // 注意第三个参数 true 开启捕获
此时点击子元素,事件在 parent 被截获,child 的点击事件将永远不会触发。
event.stopPropagation()。window 或最外层容器上监听 click 事件(设置 capture: true),并调用 event.stopPropagation(),这样内部元素都无法收到点击事件。如果同一个元素绑定了多个监听器,想把后续的监听器也一并拦住,得用 event.stopImmediatePropagation()。
浏览器会对某些事件执行默认动作。例如:
<a> 标签会跳转链接。我们可以使用 event.preventDefault() 来阻止这些默认行为。
跟不是所有事件都会冒泡一样,也并非所有事件的默认行为都能被取消。可以通过 event.cancelable 属性查看事件是否可取消。
passive 的意思是“被动”。用这个选项,等于向浏览器承诺:在这个监听器里,我绝不调用 preventDefault()。
既然你不打算阻止滚动,浏览器就不用等你的 JS 跑完再动,页面滚动自然就丝滑多了。否则,浏览器为了确认你会不会拦截滚动,每一帧都得等你,很容易卡顿。
现代浏览器为了优化体验,默认把 touchstart 和 wheel 等滚动事件设为 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 单独绑定,提高性能。
这里就得区分 target 和 currentTarget 了:
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)stopPropagation 和 preventDefault。前者阻止事件继续传播,后者阻止浏览器的默认行为(如表单提交),两者互不影响。touchstart, wheel)建议使用 passive: true,明确告知浏览器不会调用 preventDefault,从而让页面滚动更加流畅。event.target 是实际触发事件的元素,event.currentTarget 是绑定监听器的元素。在处理复杂的组件嵌套时,分清这两个属性非常重要。本期前端复习的主题是 DOM 事件机制,是前端开发非常重要的基础。除了常听到的“冒泡”,完整的事件流其实更有意思。
完整的 DOM 事件传播分为三个阶段:
window 一路向下传递到目标元素的父节点。addEventListener(type, listener, true) 第三个参数设为 true 来监听此阶段。event.target。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,不会触发 parent 或 grandparent 的监听器。
同理,如果是在捕获阶段调用 stopPropagation(),事件就无法到达目标元素和冒泡阶段:
parent.addEventListener(
"click",
(event) => {
event.stopPropagation();
console.log("parent capture");
},
true,
); // 注意第三个参数 true 开启捕获
此时点击子元素,事件在 parent 被截获,child 的点击事件将永远不会触发。
event.stopPropagation()。window 或最外层容器上监听 click 事件(设置 capture: true),并调用 event.stopPropagation(),这样内部元素都无法收到点击事件。如果同一个元素绑定了多个监听器,想把后续的监听器也一并拦住,得用 event.stopImmediatePropagation()。
浏览器会对某些事件执行默认动作。例如:
<a> 标签会跳转链接。我们可以使用 event.preventDefault() 来阻止这些默认行为。
跟不是所有事件都会冒泡一样,也并非所有事件的默认行为都能被取消。可以通过 event.cancelable 属性查看事件是否可取消。
passive 的意思是“被动”。用这个选项,等于向浏览器承诺:在这个监听器里,我绝不调用 preventDefault()。
既然你不打算阻止滚动,浏览器就不用等你的 JS 跑完再动,页面滚动自然就丝滑多了。否则,浏览器为了确认你会不会拦截滚动,每一帧都得等你,很容易卡顿。
现代浏览器为了优化体验,默认把 touchstart 和 wheel 等滚动事件设为 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 单独绑定,提高性能。
这里就得区分 target 和 currentTarget 了:
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)stopPropagation 和 preventDefault。前者阻止事件继续传播,后者阻止浏览器的默认行为(如表单提交),两者互不影响。touchstart, wheel)建议使用 passive: true,明确告知浏览器不会调用 preventDefault,从而让页面滚动更加流畅。event.target 是实际触发事件的元素,event.currentTarget 是绑定监听器的元素。在处理复杂的组件嵌套时,分清这两个属性非常重要。