🧠 一、什么是 JavaScript 的事件机制?
JavaScript 是一种 单线程语言 ,也就是说它一次只能做一件事。但网页上有很多事情是用户随时会做的(比如点击按钮、输入文字等),JavaScript 就需要一种方式来处理这些"突发事件",这就是 事件机制。
你可以把事件机制想象成一个"快递员"系统:
- 用户操作(比如点击)就像发了一个快递请求。
- JS 把这个请求放到一个队列里排队。
- 当主线程空了,JS 再一个个取出来处理。
🔁 异步执行
举个例子:
js
console.log("开始");
document.getElementById("btn").addEventListener("click", function() {
console.log("你点了按钮");
});
console.log("结束");
输出顺序是这样的:
开始
结束
(当你点按钮时才会输出)你点了按钮
解释:
console.log("开始")
和console.log("结束")
是同步代码,立刻执行。- 点击事件是异步的 ------ 只有当用户真的去点击按钮时才会执行。
✅ 所以说:事件是异步的,不会马上执行,而是等用户操作后才触发。
二、JavaScript 中的两种事件模型:DOM0 级 & DOM2 级事件
🌟 为什么需要了解这些?
JavaScript 的事件处理方式经历了几个阶段,不同的写法适用于不同年代的浏览器。现在虽然主要用 DOM2 级事件,但理解这些有助于你更好地理解 React 和现代前端框架是怎么处理事件的。
1️⃣ DOM0 级事件 ------ 最早的方式
✅ 特点:
- 直接在 HTML 标签中或 JS 中给元素添加
on事件名
属性。 - 只能绑定一个事件处理函数。
- 如果重复绑定,后面的会覆盖前面的。
示例:
html
<!-- 方式一:HTML 内联绑定 -->
<button onclick="alert('Hello')">点我</button>
<!-- 方式二:JS 中绑定 -->
<script>
const btn = document.getElementById("btn");
btn.onclick = function() {
alert("你点了按钮!");
};
</script>
缺点:
- 不够灵活,不能多次绑定同一个事件。
- 代码和结构混在一起,不便于维护。
2️⃣ DOM1 级事件? ------ 并不存在!
❓那 DOM1 是什么?
- DOM1(Document Object Model Level 1)是 W3C 发布的一个标准,主要是定义了文档的基本结构和操作方法。
- 它 并没有涉及事件处理机制。
- 所以说:DOM1 级事件这个说法其实是不存在的。
3️⃣ DOM2 级事件 ------ 推荐使用的方式
✅ 特点:
- 使用
addEventListener()
方法。 - 支持多个监听器绑定到同一个事件上。
- 可以指定是在 捕获阶段 还是 冒泡阶段 触发。
- 更加灵活、强大。
示例:
js
const btn = document.getElementById("btn");
btn.addEventListener("click", function() {
alert("第一次点击");
});
btn.addEventListener("click", function() {
alert("第二次点击");
});
两个提示都会弹出,不会互相覆盖!
捕获 vs 冒泡:
你可以把事件想象成一颗石头掉进水里:
- 捕获阶段(Capture Phase):石头往水里沉下去(从最外层向目标元素传播)。
- 冒泡阶段(Bubble Phase):水花溅上来(从目标元素向外传播)。
React 的事件命名也体现了这一点:
onClick
→ 冒泡阶段触发onClickCapture
→ 捕获阶段触发
🧩 实际应用场景
1. 事件委托
使用冒泡阶段可以非常方便地实现事件委托。例如,你可以在一个列表项很多的情况下,只给整个列表添加一个事件监听器,而不是为每一个列表项都添加监听器。
js
document.getElementById('list').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('你点击了一个列表项');
}
});
这样不仅减少了内存占用,还简化了代码逻辑。
2. 阻止事件传播
有时候你需要阻止某个事件继续传播,比如在一个模态框(Modal)内部点击时,不希望触发背后的页面点击事件。你可以通过在捕获阶段阻止事件传播来达到目的。
js
document.getElementById('modal').addEventListener('click', function(event) {
event.stopPropagation(); // 阻止事件继续冒泡
}, true); // 使用捕获阶段
🤔 为什么不只用一个阶段?
如果只保留一个阶段(无论是捕获还是冒泡),都会失去一些重要的灵活性:
- 仅捕获阶段:无法轻松实现常见的用户交互,因为大多数情况下我们希望事件能够"冒泡"上来。
- 仅冒泡阶段:则失去了在事件到达目标之前进行处理的机会,这对于某些高级功能(如全局拦截、权限控制)是必要的。
📋 总结一句话:
捕获阶段和冒泡阶段各有其独特的用途和优势,它们共同提供了强大的灵活性,使得我们可以根据不同的需求选择合适的时机来处理事件。
🔁 对比总结表
特性 | DOM0 级事件 | DOM2 级事件 |
---|---|---|
绑定方式 | onclick 属性或赋值 |
addEventListener() |
是否支持多个监听器 | 否(会被覆盖) | 是 |
是否支持捕获/冒泡控制 | 否 | 是 |
是否推荐使用 | ❌ 不推荐 | ✅ 推荐 |
是否容易维护 | ❌ 差(逻辑与结构混合) | ✅ 好 |
🧠 小贴士:React 用了哪种方式?
React 的合成事件系统底层其实用的是 DOM2 级事件 (addEventListener
),只是封装了一层,让你不用手动管理事件绑定和解绑,更加安全高效。
✅ 现代网页开发都使用
addEventListener()
来监听事件,它更灵活、功能更强,是主流做法。
三、🧩 addEventListener() 是什么?
它是 JavaScript 中用来监听事件的标准方法,比如点击、输入、滚动等。
基本写法:
js
element.addEventListener(type, listener, options);
或者:
js
element.addEventListener(type, listener, useCapture);
1. 参数详解
1) type
:要监听的事件类型(字符串)
就是你想监听哪种操作,比如:
"click"
点击"input"
输入框内容变化"scroll"
滚动页面"keydown"
键盘按键按下
✅ 注意:这个值是大小写敏感的,一般都用小写。
示例:
js
document.getElementById("btn").addEventListener("click", function() {
alert("按钮被点击了");
});
2) listener
:事件发生时执行的函数
它是一个函数或实现了 EventListener
接口的对象。
✅ 最常见的是一个函数:
js
function handleClick(event) {
console.log("你点我了");
}
button.addEventListener("click", handleClick);
⚠️ 注意:不要加括号 ()
,因为我们要传的是函数本身,不是调用结果。
3) options
或 useCapture
:可选参数,控制监听行为
这是高级功能,我们可以根据需求选择是否使用。
2. useCapture
参数(布尔值)
决定是在 捕获阶段 还是 冒泡阶段 触发事件。
true
:在捕获阶段触发(从外向内)false
:在冒泡阶段触发(从内向外)------ 默认值
示例:
html
<div id="outer">
<div id="inner">点我</div>
</div>
js
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
// 捕获阶段监听
outer.addEventListener("click", () => {
console.log("外层:捕获阶段");
}, true);
// 冒泡阶段监听
outer.addEventListener("click", () => {
console.log("外层:冒泡阶段");
}, false);
inner.addEventListener("click", () => {
console.log("内层被点击");
});
输出顺序(点击"内层"):
外层:捕获阶段
内层被点击
外层:冒泡阶段
3. options
参数对象(React 和现代项目常用)
options
是一个对象,可以包含以下选项:
属性 | 类型 | 描述 |
---|---|---|
capture |
Boolean | 同 useCapture ,是否在捕获阶段触发 |
once |
Boolean | 只触发一次,之后自动移除监听器 |
passive |
Boolean | 表示不会调用 preventDefault() ,用于优化性能(如滚动) |
signal |
AbortSignal | 配合 AbortController 使用,可以手动取消监听 |
✅ 1) once
: 只触发一次
js
button.addEventListener("click", () => {
console.log("只执行一次!");
}, { once: true });
点击第一次会输出,第二次就不会了。
✅ 2) passive
: 不阻止默认行为,提高滚动性能
适合移动端滚动优化:
js
window.addEventListener("wheel", () => {
console.log("滚动了");
}, { passive: true });
如果用了 { passive: true }
,就不能再调用 event.preventDefault()
,否则浏览器会警告。
✅ 3) signal
: 动态取消监听
配合 AbortController
使用:
js
const controller = new AbortController();
const signal = controller.signal;
button.addEventListener("click", () => {
console.log("点击了一次");
}, { signal });
controller.abort(); // 主动取消监听
4. 总结对比表
参数名 | 类型 | 作用 | 示例 |
---|---|---|---|
type |
字符串 | 事件类型,如 "click" |
"scroll" |
listener |
函数或对象 | 事件触发时执行的函数 | handleClick |
useCapture |
布尔值 | 是否在捕获阶段触发 | true / false |
options.capture |
布尔值 | 同上 | { capture: true } |
options.once |
布尔值 | 只触发一次后自动移除 | { once: true } |
options.passive |
布尔值 | 不调用 preventDefault() ,提升性能 |
{ passive: true } |
options.signal |
AbortSignal | 手动控制取消监听 | { signal } |
四、🧠 什么是事件委托?
事件委托,就是把子元素的事件监听任务交给父元素来做。
听起来有点抽象?没关系,我们用小白也能懂的方式解释。
🏠 类比理解
想象你是一个小区的保安:
- 小区里有 100 户人家。
- 如果你给每家每户门口都装一个摄像头、安排一个人看守,成本太高了。
- 所以你选择在小区大门装一个摄像头,谁进来了你都知道 ------ 这就是"事件委托"。
✅ 在网页中:
- 子元素很多的时候,每个都加监听器效率低。
- 把监听器放在它们的共同父元素上,通过冒泡机制统一处理。
✅ 举个例子
HTML 结构:
html
<ul id="menu">
<li>首页</li>
<li>关于我们</li>
<li>联系我们</li>
</ul>
你想点击每个 <li>
的时候弹出对应的菜单名。
不推荐的做法(每个都加监听器):
js
document.querySelectorAll("#menu li").forEach(item => {
item.addEventListener("click", function() {
alert(this.textContent);
});
});
推荐做法(使用事件委托):
js
document.getElementById("menu").addEventListener("click", function(event) {
if (event.target.tagName === "LI") {
alert(event.target.textContent);
}
});
🔍 原理讲解
因为事件会冒泡,所以:
- 点击的是
<li>
,但事件会一直冒泡到它的父元素<ul>
- 我们在
<ul>
上监听点击事件,然后判断event.target
是哪个元素触发的 - 如果是
<li>
,就执行对应的操作
💡 event.target 和 event.currentTarget 的区别
属性 | 含义 |
---|---|
event.target |
实际被点击的元素(比如某个 <li> ) |
event.currentTarget |
当前正在处理事件的元素(也就是绑定监听器的那个父元素) |
js
document.getElementById("menu").addEventListener("click", function(event) {
console.log("event.target:", event.target); // 被点击的 <li>
console.log("event.currentTarget:", event.currentTarget); // 绑定事件的 <ul>
});
🚀 事件委托的优点(为什么大家都喜欢用它)
优点 | 说明 |
---|---|
性能更好 | 不需要为每个子元素单独绑定事件,减少内存消耗 |
动态添加元素也有效 | 新增的 <li> 也会自动被委托处理,不需要重新绑定 |
代码更简洁 | 只需写一个监听函数,就可以处理多个子元素 |
📌 使用场景举例
- 表格、列表项很多时(如聊天消息、商品列表)
- 动态加载内容(比如 Ajax 加载新数据)
- 导航栏、选项卡等交互组件
- React 中的事件系统其实也是基于事件委托实现的!
❗ 注意事项
- 只能用于支持事件冒泡的事件类型(如 click、input),不适用于 focus、blur 等
- 要注意判断
event.target
,避免误操作其他嵌套元素(比如<li>
里面有<span>
)
事件委托是一种利用事件冒泡机制,将多个子元素的事件监听集中到父元素上的优化技术,既节省资源又便于维护。