事件机制(Event Mechanism)
JavaScript 事件机制是前端交互的核心,包括事件捕获、事件冒泡、事件委托等重要概念。
参考规范:DOM Events
📚 目录
1. 事件概述
1.1 什么是事件
事件是用户或浏览器执行的某种动作,如点击、滚动、键盘输入等。
// 事件三要素
// 1. 事件源:触发事件的元素
// 2. 事件类型:click、scroll、keydown 等
// 3. 事件处理函数:事件发生时要执行的函数1.2 事件绑定方式
// 方式 1:HTML 属性(不推荐)
// <button onclick="handleClick()">Click</button>
// 方式 2:DOM 属性(不推荐,只能绑定一个)
element.onclick = function() {
console.log('Clicked');
};
// 方式 3:addEventListener(推荐)
element.addEventListener('click', function(event) {
console.log('Clicked');
});2. 事件流
2.1 事件流的三个阶段
事件流描述了事件从触发到处理的完整过程,包含三个阶段:
- 捕获阶段(Capture Phase):从 window 到目标元素
- 目标阶段(Target Phase):在目标元素上
- 冒泡阶段(Bubble Phase):从目标元素到 window
window → document → html → body → div → button
↓ ↑
捕获阶段 冒泡阶段
2.2 事件捕获
// 在捕获阶段触发(第三个参数为 true)
element.addEventListener('click', function(event) {
console.log('Capture phase');
}, true);
// 事件捕获顺序:window → document → html → body → div → button2.3 事件冒泡
// 在冒泡阶段触发(默认,第三个参数为 false)
element.addEventListener('click', function(event) {
console.log('Bubble phase');
});
// 事件冒泡顺序:button → div → body → html → document → window2.4 阻止事件传播
element.addEventListener('click', function(event) {
// 阻止事件继续传播(捕获或冒泡)
event.stopPropagation();
// 阻止立即传播(立即停止,包括同一元素的其他监听器)
event.stopImmediatePropagation();
});2.5 事件目标
element.addEventListener('click', function(event) {
// event.target - 实际触发事件的元素
console.log('Target:', event.target);
// event.currentTarget - 当前处理事件的元素(this 指向)
console.log('Current target:', event.currentTarget);
// event.target === event.currentTarget 时,事件在目标元素上触发
});3. 事件对象
3.1 Event 对象
所有事件对象都继承自 Event 对象。
element.addEventListener('click', function(event) {
// 事件类型
event.type; // "click"
// 事件目标
event.target; // 触发事件的元素
event.currentTarget; // 当前处理事件的元素
// 事件阶段
event.eventPhase; // 1=捕获, 2=目标, 3=冒泡
// 时间戳
event.timeStamp; // 事件发生的时间戳
// 是否可冒泡
event.bubbles; // true/false
// 是否可取消
event.cancelable; // true/false
});3.2 阻止默认行为
// 阻止默认行为(如链接跳转、表单提交)
element.addEventListener('click', function(event) {
event.preventDefault();
// 检查是否已阻止
event.defaultPrevented; // true
});
// 示例:阻止链接跳转
link.addEventListener('click', function(event) {
event.preventDefault();
console.log('Link click prevented');
});3.3 鼠标事件对象
element.addEventListener('click', function(event) {
// 鼠标位置(相对于视口)
event.clientX; // X 坐标
event.clientY; // Y 坐标
// 鼠标位置(相对于页面)
event.pageX; // X 坐标
event.pageY; // Y 坐标
// 鼠标位置(相对于屏幕)
event.screenX; // X 坐标
event.screenY; // Y 坐标
// 鼠标按钮
event.button; // 0=左键, 1=中键, 2=右键
event.buttons; // 按下的按钮组合(位掩码)
// 修饰键
event.ctrlKey; // Ctrl 键
event.shiftKey; // Shift 键
event.altKey; // Alt 键
event.metaKey; // Meta 键(Mac 的 Cmd)
});3.4 键盘事件对象
element.addEventListener('keydown', function(event) {
// 按键代码
event.key; // "a", "Enter", "ArrowUp" 等(推荐)
event.code; // "KeyA", "Enter", "ArrowUp" 等
event.keyCode; // 数字代码(已废弃)
// 修饰键
event.ctrlKey;
event.shiftKey;
event.altKey;
event.metaKey;
// 是否重复按键
event.repeat; // true/false
});4. 事件委托
4.1 什么是事件委托
事件委托(Event Delegation)是将事件监听器绑定到父元素,利用事件冒泡机制处理子元素的事件。
4.2 事件委托的优势
// ❌ 不好:为每个子元素绑定事件
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// ✅ 好:事件委托
const list = document.querySelector('.list');
list.addEventListener('click', function(event) {
// event.target 是实际点击的元素
if (event.target.matches('.item')) {
handleClick(event);
}
});优势:
- 减少事件监听器数量(性能更好)
- 动态添加的元素自动支持事件
- 减少内存占用
4.3 事件委托实现
// 方式 1:使用 matches
parent.addEventListener('click', function(event) {
if (event.target.matches('.item')) {
console.log('Item clicked');
}
});
// 方式 2:使用 closest(向上查找)
parent.addEventListener('click', function(event) {
const item = event.target.closest('.item');
if (item) {
console.log('Item clicked:', item);
}
});
// 方式 3:检查类名
parent.addEventListener('click', function(event) {
if (event.target.classList.contains('item')) {
console.log('Item clicked');
}
});5. 事件类型
5.1 鼠标事件
// click - 单击
element.addEventListener('click', handler);
// dblclick - 双击
element.addEventListener('dblclick', handler);
// mousedown - 鼠标按下
element.addEventListener('mousedown', handler);
// mouseup - 鼠标释放
element.addEventListener('mouseup', handler);
// mousemove - 鼠标移动
element.addEventListener('mousemove', handler);
// mouseenter - 鼠标进入(不冒泡)
element.addEventListener('mouseenter', handler);
// mouseleave - 鼠标离开(不冒泡)
element.addEventListener('mouseleave', handler);
// mouseover - 鼠标悬停(冒泡)
element.addEventListener('mouseover', handler);
// mouseout - 鼠标移出(冒泡)
element.addEventListener('mouseout', handler);
// contextmenu - 右键菜单
element.addEventListener('contextmenu', handler);5.2 键盘事件
// keydown - 按键按下
element.addEventListener('keydown', handler);
// keyup - 按键释放
element.addEventListener('keyup', handler);
// keypress - 按键按下(已废弃,不推荐)
element.addEventListener('keypress', handler);5.3 表单事件
// submit - 表单提交
form.addEventListener('submit', function(event) {
event.preventDefault(); // 阻止默认提交
});
// change - 值改变(失去焦点时)
input.addEventListener('change', handler);
// input - 值改变(实时)
input.addEventListener('input', handler);
// focus - 获得焦点
input.addEventListener('focus', handler);
// blur - 失去焦点
input.addEventListener('blur', handler);5.4 滚动事件
// scroll - 滚动
window.addEventListener('scroll', function(event) {
console.log('Scrolled:', window.scrollY);
});
// ⚠️ 性能优化:使用节流
let ticking = false;
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(function() {
// 滚动处理逻辑
ticking = false;
});
ticking = true;
}
});5.5 窗口事件
// resize - 窗口大小改变
window.addEventListener('resize', handler);
// load - 页面加载完成
window.addEventListener('load', handler);
// DOMContentLoaded - DOM 加载完成
document.addEventListener('DOMContentLoaded', handler);
// beforeunload - 页面卸载前
window.addEventListener('beforeunload', handler);6. 自定义事件
6.1 创建自定义事件
// 方式 1:使用 Event 构造函数
const event = new Event('customEvent', {
bubbles: true,
cancelable: true
});
// 方式 2:使用 CustomEvent 构造函数(可传递数据)
const customEvent = new CustomEvent('customEvent', {
detail: { message: 'Hello' },
bubbles: true,
cancelable: true
});
// 触发事件
element.dispatchEvent(customEvent);6.2 监听自定义事件
element.addEventListener('customEvent', function(event) {
console.log('Custom event:', event.detail);
});6.3 事件总线模式
// 简单的事件总线实现
class EventBus {
constructor() {
this.events = {};
}
on(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}
off(event, handler) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(h => h !== handler);
}
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(handler => handler(data));
}
}
}
// 使用
const bus = new EventBus();
bus.on('userLogin', (user) => console.log('User logged in:', user));
bus.emit('userLogin', { name: 'John' });7. 事件性能优化
7.1 事件节流(Throttle)
// 节流:限制函数执行频率
function throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
// 使用
window.addEventListener('scroll', throttle(function() {
console.log('Scrolled');
}, 100));7.2 事件防抖(Debounce)
// 防抖:延迟执行,在连续触发时只执行最后一次
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用
input.addEventListener('input', debounce(function() {
console.log('Input changed');
}, 300));7.3 被动事件监听器
// passive: true - 提升滚动性能
// 告诉浏览器事件处理函数不会调用 preventDefault()
element.addEventListener('touchstart', handler, {
passive: true
});
// 检查是否支持 passive
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
supportsPassive = true;
}
});
window.addEventListener('test', null, opts);
} catch (e) {}7.4 一次性事件监听器
// once: true - 只执行一次
element.addEventListener('click', handler, {
once: true
});
// 手动移除
element.addEventListener('click', function handler(event) {
console.log('Clicked');
element.removeEventListener('click', handler);
});