网站
Event: stopPropagation() 方法 | MDN
引入
点击页面上一个按钮时,不只是按钮收到了事件,它的父元素、祖父元素,一直到 document,都会”知道”这个事件发生了。
这就是事件传播。理解事件传播机制,才能写出更灵活的事件处理代码。
正文
定义
定义
事件传播是指一个事件发生时,浏览器按照固定的顺序把事件依次传递给相关的 DOM 元素。分为三个阶段:捕获阶段、目标阶段、冒泡阶段。
三个阶段
点击一个按钮时,事件的传递过程如下:
1. 捕获阶段:从 window → document → html → body → ... → 按钮的父元素
2. 目标阶段:到达按钮本身
3. 冒泡阶段:从按钮的父元素 → ... → body → html → document → window
形象地说,事件像水波一样:先从外往里”沉”到目标,再从里往外”冒”回最外层。
默认情况下,addEventListener() 绑定的是冒泡阶段。
冒泡
冒泡是指事件从触发元素开始,逐层向上传递给父元素。
<div id="outer">
<div id="inner">
<button id="btn">点击</button>
</div>
</div>const outer = document.querySelector("#outer");
const inner = document.querySelector("#inner");
const btn = document.querySelector("#btn");
btn.addEventListener("click", function () {
console.log("按钮被点击");
});
inner.addEventListener("click", function () {
console.log("inner 被点击");
});
outer.addEventListener("click", function () {
console.log("outer 被点击");
});点击按钮后,输出顺序:
按钮被点击
inner 被点击
outer 被点击
事件从按钮冒泡到 inner,再冒泡到 outer,三个处理函数依次执行。
event.stopPropagation()
如果想阻止事件继续向上冒泡,可以调用 event.stopPropagation()。
btn.addEventListener("click", function (event) {
event.stopPropagation();
console.log("按钮被点击,停止冒泡");
});这时点击按钮,只输出”按钮被点击,停止冒泡”,inner 和 outer 的监听不会触发。
注意:滥用 stopPropagation() 会让事件流变得不透明,调试困难。大部分场景下不需要手动停止冒泡。
event.target vs event.currentTarget
事件传播过程中,需要区分两个属性:
| 属性 | 含义 |
|---|---|
event.target | 真正触发事件的元素(不会变) |
event.currentTarget | 当前正在执行处理函数的元素(会变) |
<div id="box" style="padding: 20px; background: lightblue;">
<button id="btn">按钮</button>
</div>const box = document.querySelector("#box");
box.addEventListener("click", function (event) {
console.log("target:", event.target.id);
console.log("currentTarget:", event.currentTarget.id);
});点击按钮时:
event.target是btn(真正被点击的元素)event.currentTarget是box(绑定事件的元素)
点击 box 的空白区域时:
event.target是boxevent.currentTarget也是box
事件委托
事件委托是利用冒泡机制,把事件绑定在父元素上,通过 event.target 判断实际触发事件的子元素。
不用事件委托时,需要给每个子元素单独绑定事件:
<ul id="list">
<li>项目 1</li>
<li>项目 2</li>
<li>项目 3</li>
</ul>const items = document.querySelectorAll("li");
items.forEach(function (item) {
item.addEventListener("click", function () {
console.log("点击了:" + item.textContent);
});
});使用事件委托,只需要在 <ul> 上绑定一次:
const list = document.querySelector("#list");
list.addEventListener("click", function (event) {
if (event.target.tagName === "LI") {
console.log("点击了:" + event.target.textContent);
}
});效果一样,但只绑定了一次事件。
事件委托的好处
| 对比 | 不用委托 | 用委托 |
|---|---|---|
| 绑定次数 | N 个元素绑定 N 次 | 父元素绑定 1 次 |
| 内存占用 | 每个元素都存回调函数 | 只存一个回调函数 |
| 动态元素 | 新元素需要重新绑定 | 自动生效 |
例子
动态列表的事件委托
<input id="input" type="text" placeholder="输入内容">
<button id="addBtn">添加</button>
<ul id="list"></ul>const input = document.querySelector("#input");
const addBtn = document.querySelector("#addBtn");
const list = document.querySelector("#list");
addBtn.addEventListener("click", function () {
if (input.value === "") {
return;
}
const li = document.createElement("li");
li.textContent = input.value;
list.appendChild(li);
input.value = "";
});
list.addEventListener("click", function (event) {
if (event.target.tagName === "LI") {
event.target.remove();
}
});点击”添加”新增列表项,点击列表项删除它。
即使列表项是动态创建的,事件委托也能正常工作,不需要给每个新 <li> 重新绑定事件。
判断点击的是哪个子元素
<div id="toolbar">
<button data-action="bold">加粗</button>
<button data-action="italic">斜体</button>
<button data-action="underline">下划线</button>
</div>const toolbar = document.querySelector("#toolbar");
toolbar.addEventListener("click", function (event) {
if (event.target.tagName === "BUTTON") {
const action = event.target.getAttribute("data-action");
console.log("执行操作:" + action);
}
});只在父元素上绑定一次事件,通过 event.target 和 data-action 属性判断用户点了哪个按钮。
捕获阶段
addEventListener() 默认在冒泡阶段触发。传入第三个参数 true 可以让它在捕获阶段触发。
outer.addEventListener("click", function () {
console.log("outer 捕获阶段");
}, true);
inner.addEventListener("click", function () {
console.log("inner 捕获阶段");
}, true);
btn.addEventListener("click", function () {
console.log("按钮被点击");
});点击按钮后输出:
outer 捕获阶段
inner 捕获阶段
按钮被点击
捕获阶段是从外往内,所以 outer 先于 inner 执行。
实际开发中很少用到捕获阶段,了解即可。
特点
- 事件传播分三个阶段:捕获 → 目标 → 冒泡
addEventListener()默认在冒泡阶段触发- 事件会从触发元素逐层向上传递给父元素
event.target是真正触发事件的元素,event.currentTarget是当前绑定事件的元素event.stopPropagation()可以阻止事件继续传播- 事件委托利用冒泡,把事件绑定在父元素上
- 事件委托对动态添加的元素也有效,不需要重新绑定
- 捕获阶段很少用到,了解即可
注意事项
- 不是所有事件都会冒泡,
focus、blur、scroll不冒泡 focus和blur有冒泡版本:focusin和focusout- 频繁调用
stopPropagation()会让事件流变得难以预测 - 事件委托中判断
event.target时,要注意点击的可能是子元素内部的更深层元素 - 可以用
event.target.closest("li")来查找最近的匹配祖先元素
理解
可以把事件传播理解成”回声”。
在房间里喊一句话,声音从你身边(目标)传出去,经过走廊(父元素),最后到达大厅(document)。
事件委托就像在大厅装了一个麦克风,不管哪个房间有人说话,大厅都能听到,再通过声音特征判断是谁说的。
我理解事件传播的重点是:事件会从子元素冒泡到父元素,利用这个特性可以在父元素上统一处理子元素的事件。