本期前端复习的主题是 DOM 事件机制,是前端开发非常重要的基础。除了常听到的“冒泡”,完整的事件流其实更有意思。
事件传播
完整的 DOM 事件传播分为三个阶段:
- 捕获阶段(Capturing Phase)
- 事件从
window一路向下传递到目标元素的父节点。 - 期间可以通过
addEventListener(type, listener, true)第三个参数设为true来监听此阶段。
- 事件从
- 目标阶段(Target Phase)
- 事件到达目标元素本身,即
event.target。 - 此阶段监听函数会被触发。
- 事件到达目标元素本身,即
- 冒泡阶段(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,不会触发 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
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是绑定监听器的元素。在处理复杂的组件嵌套时,分清这两个属性非常重要。