网站

事件传播 | MDN

Event: stopPropagation() 方法 | MDN

事件委托 | 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("按钮被点击,停止冒泡");
});

这时点击按钮,只输出”按钮被点击,停止冒泡”,innerouter 的监听不会触发。

注意:滥用 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.targetbtn(真正被点击的元素)
  • event.currentTargetbox(绑定事件的元素)

点击 box 的空白区域时:

  • event.targetbox
  • event.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.targetdata-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() 可以阻止事件继续传播
  • 事件委托利用冒泡,把事件绑定在父元素上
  • 事件委托对动态添加的元素也有效,不需要重新绑定
  • 捕获阶段很少用到,了解即可

注意事项

  • 不是所有事件都会冒泡,focusblurscroll 不冒泡
  • focusblur 有冒泡版本:focusinfocusout
  • 频繁调用 stopPropagation() 会让事件流变得难以预测
  • 事件委托中判断 event.target 时,要注意点击的可能是子元素内部的更深层元素
  • 可以用 event.target.closest("li") 来查找最近的匹配祖先元素

理解

可以把事件传播理解成”回声”。

在房间里喊一句话,声音从你身边(目标)传出去,经过走廊(父元素),最后到达大厅(document)。

事件委托就像在大厅装了一个麦克风,不管哪个房间有人说话,大厅都能听到,再通过声音特征判断是谁说的。

我理解事件传播的重点是:事件会从子元素冒泡到父元素,利用这个特性可以在父元素上统一处理子元素的事件

引出

理解了事件传播之后,可以深入学习 JS 事件 的基础用法,或者学习 JS 表单操作,了解表单数据的获取和处理。