第 28 章 事件基础

第 28 章 事件基础

想象一下,你在网上商城买了一件衣服,点击了"立即购买"按钮,然后…什么都没发生。你以为坏了,但其实是 JavaScript 在"等待指令"。事件就是 JavaScript 和用户之间的"对讲机"——用户做点什么(比如点击、输入、移动鼠标),JavaScript 就收到"信号"然后做出响应。

28.1 事件绑定

onclick = function(){}:同类型只能绑定一个,会覆盖

onclick 是最古老、最简单的绑定事件的方式。但它有一个致命缺陷——同一种事件类型只能绑定一个处理函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 获取按钮元素
const button = document.getElementById('myButton');

// 第一次绑定
button.onclick = function() {
    console.log('第一次绑定的处理函数');
};

// 第二次绑定 —— 第一个函数被"覆盖"了!
button.onclick = function() {
    console.log('第二次绑定的处理函数');
};

button.click();
// 打印结果: 第二次绑定的处理函数(只有这一个,第一个永远不会被调用)

实际案例:假设你想让按钮点击两次后失效,用 onclick 实现会很麻烦。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const button = document.getElementById('myButton');
let clickCount = 0;

button.onclick = function() {
    clickCount++;
    console.log('点击了 ' + clickCount + ' 次');
    
    if (clickCount >= 2) {
        // 想禁用,但 onclick 的问题在于...
        // 你没法"追加"处理函数,只能替换
        button.onclick = null; // 直接清空
    }
};

addEventListener():可绑定多个,不覆盖,推荐使用

addEventListener 是现代浏览器推荐的事件绑定方式。它最大的优点是——可以绑定多个处理函数,它们会依次执行,不会覆盖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const button = document.getElementById('myButton');

// 第一个处理函数
button.addEventListener('click', function() {
    console.log('处理函数 A');
});

// 第二个处理函数 —— 不会覆盖 A
button.addEventListener('click', function() {
    console.log('处理函数 B');
});

button.click();
// 打印结果: 处理函数 A
// 处理函数 B

addEventListener 的语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 完整语法
element.addEventListener(eventType, handler, options);

// eventType: 事件类型,如 'click'、'mouseover'(不带 on 前缀)
// handler: 处理函数
// options: 可选配置对象

// 示例
button.addEventListener('click', function(event) {
    console.log('按钮被点击了');
}, false);

// 第三个参数为 false 表示在冒泡阶段处理(默认)

removeEventListener():解绑(匿名函数无法解绑,需保存函数引用)

当你不再需要某个事件处理函数时,应该把它解绑,否则可能会造成内存泄漏和意外的 bug。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const button = document.getElementById('myButton');

// 定义一个命名函数
function handleClick() {
    console.log('点击了一次');
    // 满足某个条件后,解除绑定
    if (clickCount >= 3) {
        button.removeEventListener('click', handleClick);
        console.log('已解除事件绑定');
    }
    clickCount++;
}

let clickCount = 0;
button.addEventListener('click', handleClick);

❌ 匿名函数无法解绑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ❌ 匿名函数的问题:无法解绑
button.addEventListener('click', function() {
    console.log('匿名函数');
});

// 没有办法解绑这个函数!
// 因为你无法引用它

// ✅ 解决:使用具名函数或保存引用
const handler = function() {
    console.log('可以解绑');
};
button.addEventListener('click', handler);
button.removeEventListener('click', handler);

onclick vs addEventListener 对比

特性onclickaddEventListener
绑定多个处理函数❌ 会覆盖✅ 依次执行
解绑❌ 不能✅ 可以
支持事件捕获❌ 不支持✅ 支持
支持选项配置❌ 不支持✅ 支持
兼容性IE6+IE9+(IE8 用 attachEvent)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// onclick 用法
button.onclick = function() {
    console.log('onclick 方式');
};

// addEventListener 方式
button.addEventListener('click', function() {
    console.log('addEventListener 方式');
});

// onclick 只支持冒泡阶段,addEventListener 支持捕获阶段
// 第三个参数 true 表示捕获阶段处理
button.addEventListener('click', function() {
    console.log('捕获阶段');
}, true);

// 第三个参数 false 表示冒泡阶段处理(默认)
button.addEventListener('click', function() {
    console.log('冒泡阶段');
}, false);

下一节,我们来学习事件对象!

28.2 事件对象

当事件触发时,浏览器会自动创建一个事件对象(Event Object),包含了很多有用的信息,比如:触发事件的元素是什么?鼠标在哪里点击的?按了什么键?

e.target:触发事件的元素

target 是触发事件的源头元素,也就是你实际点击的那个元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 事件委托场景:给父元素绑定事件,通过 target 判断点击了哪个子元素
const container = document.getElementById('container');

// 点击按钮、div、span 都会触发这个事件
container.addEventListener('click', function(event) {
    console.log('触发事件的元素是:', event.target);
    console.log('元素标签名:', event.target.tagName);
    console.log('元素 ID:', event.target.id);
    console.log('元素内容:', event.target.textContent);
});

e.currentTarget:绑定事件的元素

currentTarget绑定事件的元素,也就是绑定了这个事件监听器的元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 对比 target 和 currentTarget
const parent = document.getElementById('parent');
const child = document.getElementById('child');

parent.addEventListener('click', function(event) {
    console.log('target:', event.target.id);       // 实际点击的元素
    console.log('currentTarget:', event.currentTarget.id); // 绑定事件的元素
});

child.addEventListener('click', function(event) {
    // 如果 child 也绑定了事件
    console.log('target:', event.target.id);
    console.log('currentTarget:', event.currentTarget.id);
});

// 点击 child 时:
// child 自己的监听器:target=child, currentTarget=child
// parent 的监听器:target=child, currentTarget=parent

e.type:事件类型

type 告诉你这是什么类型的事件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const button = document.getElementById('myButton');

button.addEventListener('click', function(event) {
    console.log('事件类型是:', event.type); // 打印结果: click
});

button.addEventListener('mouseover', function(event) {
    console.log('事件类型是:', event.type); // 打印结果: mouseover
});

// 一个通用的日志函数
function logEvent(event) {
    console.log('事件类型: ' + event.type + ', 目标元素: ' + event.target.tagName);
}

e.preventDefault():阻止默认行为

有些元素有默认行为,比如链接会跳转、表单会提交。preventDefault() 可以阻止这些默认行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 阻止链接跳转
document.querySelector('a.no-follow').addEventListener('click', function(event) {
    event.preventDefault(); // 阻止跳转
    console.log('链接跳转被阻止了');
});

// 阻止表单提交
document.querySelector('form').addEventListener('submit', function(event) {
    event.preventDefault(); // 阻止提交
    console.log('表单提交被阻止了');
});

// 阻止右键菜单
document.addEventListener('contextmenu', function(event) {
    event.preventDefault();
    console.log('右键菜单被阻止了');
});

// 阻止文本选择
document.querySelector('.no-select').addEventListener('selectstart', function(event) {
    event.preventDefault();
});

e.stopPropagation():阻止冒泡

事件会从触发元素向上传播到父元素,这叫"冒泡"。stopPropagation() 可以阻止这个传播过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const parent = document.getElementById('parent');
const child = document.getElementById('child');

parent.addEventListener('click', function() {
    console.log('parent 被点击了');
});

child.addEventListener('click', function(event) {
    console.log('child 被点击了');
    event.stopPropagation(); // 阻止事件继续向上冒泡
});

child.click();
// 打印结果: child 被点击了
// parent 的点击事件不会被触发!

e.stopImmediatePropagation():阻止后续同类型事件

如果一个元素绑定了多个同类型事件,stopImmediatePropagation() 不仅阻止冒泡,还会阻止后续的处理函数执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const button = document.getElementById('myButton');

button.addEventListener('click', function(event) {
    console.log('处理函数 1');
});

button.addEventListener('click', function(event) {
    console.log('处理函数 2');
    event.stopImmediatePropagation(); // 不仅阻止冒泡,还阻止后续处理函数
});

button.addEventListener('click', function(event) {
    console.log('处理函数 3 —— 不会执行');
});

button.click();
// 打印结果: 处理函数 1
// 处理函数 2
// 处理函数 3 不会打印!

下一节,我们来学习事件流!

28.3 事件流

三个阶段:捕获阶段 → 目标阶段 → 冒泡阶段

当一个事件触发时,它会经历三个阶段:

flowchart TD
    A["事件触发"] --> B["捕获阶段<br/>从 window 向下到目标元素"]
    B --> C["目标阶段<br/>事件到达目标元素"]
    C --> D["冒泡阶段<br/>从目标元素向上到 window"]
    
    style B fill:#fff3cd
    style C fill:#d4edda
    style D fill:#cce5ff
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 三个阶段的监听器
document.addEventListener('click', function() {
    console.log('捕获阶段 - document');
}, true); // true 表示在捕获阶段处理

window.addEventListener('click', function() {
    console.log('捕获阶段 - window');
}, true);

const button = document.getElementById('myButton');

button.addEventListener('click', function() {
    console.log('目标阶段 - button');
}, true); // 目标阶段

button.addEventListener('click', function() {
    console.log('目标阶段 - button(冒泡)');
}, false); // false 或不传表示在冒泡阶段处理

document.addEventListener('click', function() {
    console.log('冒泡阶段 - document');
}, false);

事件捕获:addEventListener 第三个参数为 true

默认情况下,事件处理在冒泡阶段执行。如果想在捕获阶段处理,第三个参数传 true

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const parent = document.getElementById('parent');
const child = document.getElementById('child');

// 捕获阶段:外层先处理,内层后处理
parent.addEventListener('click', function() {
    console.log('parent 捕获阶段');
}, true);

child.addEventListener('click', function() {
    console.log('child 捕获阶段');
}, true);

child.click();
// 打印结果: parent 捕获阶段 -> child 捕获阶段

事件冒泡:默认行为,事件向上传播

冒泡是事件的默认行为。当子元素的事件触发时,父元素同类型的事件也会被触发。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');

outer.addEventListener('click', function() {
    console.log('outer 冒泡');
});

inner.addEventListener('click', function() {
    console.log('inner 冒泡');
});

inner.click();
// 打印结果:
// inner 冒泡
// outer 冒泡
// 事件从内向外"冒泡"

事件委托:绑定到父元素,利用冒泡处理子元素事件

事件委托是一种高级技巧——把事件绑定到父元素,利用冒泡机制统一处理子元素的事件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ❌ 传统方式:给每个 li 都绑定事件
document.querySelectorAll('li').forEach(function(li) {
    li.addEventListener('click', function() {
        console.log('点击了:' + this.textContent);
    });
});

// ✅ 事件委托方式:只绑定一个事件
document.querySelector('ul').addEventListener('click', function(event) {
    // event.target 是实际点击的元素
    if (event.target.tagName === 'LI') {
        console.log('点击了:' + event.target.textContent);
    }
});

事件委托的优点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 1. 性能优化:减少事件绑定数量
// 假设有一个 1000 项的列表
// ❌ 传统方式:绑定 1000 个事件
// ✅ 事件委托:只绑定 1 个事件

// 2. 动态内容支持:新增的子元素自动获得事件
const list = document.getElementById('list');
list.addEventListener('click', function(event) {
    if (event.target.classList.contains('item')) {
        console.log('点击了:' + event.target.textContent);
    }
});

// 新增的 li 不用单独绑定事件
const newItem = document.createElement('li');
newItem.className = 'item';
newItem.textContent = '新项目';
list.appendChild(newItem); // 自动就有点击事件了!

本章小结

本章我们学习了 JavaScript 事件的基础知识:

  1. 事件绑定onclick 简单但会覆盖,addEventListener 可绑定多个且可解绑。
  2. 事件对象:target、currentTarget、type、preventDefault、stopPropagation、stopImmediatePropagation。
  3. 事件流:三个阶段——捕获阶段、目标阶段、冒泡阶段。
  4. 事件委托:利用冒泡机制,在父元素统一处理子元素的事件,性能高且支持动态内容。

下一章,我们要学习各种具体的事件类型——鼠标事件、键盘事件、表单事件等!