Web API基础-事件处理与委托
# Web API基础-事件处理与委托
事件是JavaScript与用户交互的核心机制。通过事件,我们可以响应用户的点击、输入、滚动等操作。本文将深入讲解事件处理的各个方面,包括事件监听、事件对象、事件传播、事件委托等内容。
# 一、事件基础
# 1.1 什么是事件
事件是用户或浏览器执行的某种动作,例如:
- 用户交互:
click、dblclick、mousedown、keypress、scroll - 页面生命周期:
DOMContentLoaded、load、beforeunload - 表单相关:
submit、input、change、focus、blur - 媒体相关:
play、pause、ended
# 1.2 添加事件监听器
// 方式1:HTML属性(不推荐)
<button onclick="handleClick()">点击</button>
// 方式2:DOM属性(会覆盖)
const btn = document.querySelector('button');
btn.onclick = function() {
console.log('点击了');
};
// 方式3:addEventListener(推荐)
btn.addEventListener('click', function(e) {
console.log('点击了', e);
});
// 可以添加多个监听器
btn.addEventListener('click', handler1);
btn.addEventListener('click', handler2); // 不会覆盖handler1
# 1.3 移除事件监听器
function handleClick(e) {
console.log('点击了');
}
const btn = document.querySelector('button');
// 添加监听器
btn.addEventListener('click', handleClick);
// 移除监听器(必须是同一个函数引用)
btn.removeEventListener('click', handleClick);
// ✗ 无法移除:匿名函数
btn.addEventListener('click', function() {
console.log('点击');
});
btn.removeEventListener('click', function() {
console.log('点击');
}); // 无效,不是同一个函数引用
# 1.4 addEventListener选项
element.addEventListener('click', handler, {
capture: false, // 是否在捕获阶段触发
once: true, // 只触发一次后自动移除
passive: true, // 不会调用preventDefault()
signal: abortController.signal // 用于批量移除
});
// 简写形式
element.addEventListener('click', handler, true); // capture: true
// 使用AbortController批量移除
const controller = new AbortController();
btn1.addEventListener('click', handler1, { signal: controller.signal });
btn2.addEventListener('click', handler2, { signal: controller.signal });
btn3.addEventListener('click', handler3, { signal: controller.signal });
// 一次性移除所有
controller.abort();
# 二、事件对象
# 2.1 Event对象属性
element.addEventListener('click', function(e) {
// 事件类型
console.log(e.type); // "click"
// 事件目标
console.log(e.target); // 实际触发事件的元素
console.log(e.currentTarget); // 绑定事件的元素
// 时间戳
console.log(e.timeStamp); // 事件发生的时间
// 事件阶段
console.log(e.eventPhase);
// 1: 捕获阶段
// 2: 目标阶段
// 3: 冒泡阶段
// 是否可冒泡
console.log(e.bubbles); // true/false
// 是否可取消
console.log(e.cancelable); // true/false
});
# 2.2 鼠标事件对象
element.addEventListener('click', function(e) {
// 鼠标位置(相对于视口)
console.log(e.clientX, e.clientY);
// 鼠标位置(相对于页面)
console.log(e.pageX, e.pageY);
// 鼠标位置(相对于屏幕)
console.log(e.screenX, e.screenY);
// 鼠标位置(相对于目标元素)
console.log(e.offsetX, e.offsetY);
// 按下的鼠标按钮
console.log(e.button);
// 0: 左键
// 1: 中键
// 2: 右键
// 修饰键
console.log(e.ctrlKey); // 是否按下Ctrl
console.log(e.shiftKey); // 是否按下Shift
console.log(e.altKey); // 是否按下Alt
console.log(e.metaKey); // 是否按下Meta(Mac的Cmd)
// 相关元素(mouseover/mouseout)
console.log(e.relatedTarget);
});
# 2.3 键盘事件对象
element.addEventListener('keydown', function(e) {
// 按键代码
console.log(e.key); // 按键字符,如 "a", "Enter", "ArrowUp"
console.log(e.code); // 物理按键,如 "KeyA", "Enter", "ArrowUp"
console.log(e.keyCode); // 已废弃,不推荐使用
// 修饰键
console.log(e.ctrlKey);
console.log(e.shiftKey);
console.log(e.altKey);
console.log(e.metaKey);
// 是否重复触发(长按)
console.log(e.repeat);
// 组合键判断
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('Ctrl+S 保存');
}
});
# 2.4 表单事件对象
const input = document.querySelector('input');
input.addEventListener('input', function(e) {
// 输入值
console.log(e.target.value);
// inputType(详细输入类型)
console.log(e.inputType);
// "insertText", "deleteContentBackward" 等
});
input.addEventListener('change', function(e) {
console.log('值改变了:', e.target.value);
});
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
e.preventDefault(); // 阻止表单提交
const formData = new FormData(e.target);
console.log(Object.fromEntries(formData));
});
# 三、事件传播
# 3.1 事件流三个阶段
事件传播分为三个阶段:
1. 捕获阶段(Capturing):从window到目标元素
2. 目标阶段(Target):到达目标元素
3. 冒泡阶段(Bubbling):从目标元素返回window
<div id="outer">
<div id="middle">
<div id="inner">点击我</div>
</div>
</div>
<script>
const outer = document.getElementById('outer');
const middle = document.getElementById('middle');
const inner = document.getElementById('inner');
// 冒泡阶段(默认)
outer.addEventListener('click', () => console.log('outer 冒泡'));
middle.addEventListener('click', () => console.log('middle 冒泡'));
inner.addEventListener('click', () => console.log('inner 冒泡'));
// 捕获阶段
outer.addEventListener('click', () => console.log('outer 捕获'), true);
middle.addEventListener('click', () => console.log('middle 捕获'), true);
inner.addEventListener('click', () => console.log('inner 捕获'), true);
// 点击inner,输出顺序:
// outer 捕获
// middle 捕获
// inner 捕获
// inner 冒泡
// middle 冒泡
// outer 冒泡
</script>
# 3.2 阻止事件传播
element.addEventListener('click', function(e) {
// 阻止事件继续传播(冒泡或捕获)
e.stopPropagation();
// 立即阻止事件传播(同一元素的其他监听器也不会执行)
e.stopImmediatePropagation();
});
<div id="parent">
<button id="child">点击</button>
</div>
<script>
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', () => {
console.log('父元素点击');
});
child.addEventListener('click', (e) => {
console.log('子元素点击');
e.stopPropagation(); // 阻止冒泡到父元素
});
// 点击button,只输出:子元素点击
</script>
# 3.3 阻止默认行为
// 阻止链接跳转
document.querySelector('a').addEventListener('click', function(e) {
e.preventDefault();
console.log('链接被阻止');
});
// 阻止表单提交
document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();
console.log('表单提交被阻止');
});
// 阻止右键菜单
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
// 检查是否可以阻止
if (e.cancelable) {
e.preventDefault();
}
# 四、事件委托
# 4.1 什么是事件委托
事件委托利用事件冒泡机制,将事件监听器添加到父元素上,通过判断e.target来处理子元素的事件。
优势:
- 减少内存占用(只需一个监听器)
- 动态元素自动绑定事件
- 提升性能
# 4.2 基础事件委托
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
<script>
// ✗ 不推荐:为每个li添加监听器
const items = document.querySelectorAll('li');
items.forEach(item => {
item.addEventListener('click', function() {
console.log(this.textContent);
});
});
// ✓ 推荐:事件委托
const list = document.getElementById('list');
list.addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log(e.target.textContent);
}
});
</script>
# 4.3 处理动态元素
const list = document.getElementById('list');
// 事件委托
list.addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log('点击了:', e.target.textContent);
}
});
// 动态添加元素
const newItem = document.createElement('li');
newItem.textContent = '新项目';
list.appendChild(newItem); // 自动具有点击事件
# 4.4 高级事件委托
<div id="container">
<button class="action" data-action="edit">编辑</button>
<button class="action" data-action="delete">删除</button>
<button class="action" data-action="share">分享</button>
</div>
<script>
const container = document.getElementById('container');
container.addEventListener('click', function(e) {
// 使用closest查找最近的匹配元素
const actionBtn = e.target.closest('.action');
if (!actionBtn) return;
const action = actionBtn.dataset.action;
switch(action) {
case 'edit':
console.log('编辑操作');
break;
case 'delete':
console.log('删除操作');
break;
case 'share':
console.log('分享操作');
break;
}
});
</script>
# 4.5 事件委托工具函数
function delegate(parent, selector, eventType, handler) {
parent.addEventListener(eventType, function(e) {
const target = e.target.closest(selector);
if (target && parent.contains(target)) {
handler.call(target, e);
}
});
}
// 使用示例
delegate(document.body, '.delete-btn', 'click', function(e) {
console.log('删除按钮被点击', this);
});
delegate(document.body, '.item', 'mouseenter', function(e) {
this.classList.add('hover');
});
# 五、常用事件类型
# 5.1 鼠标事件
// 点击事件
element.addEventListener('click', handler); // 单击
element.addEventListener('dblclick', handler); // 双击
element.addEventListener('contextmenu', handler); // 右键菜单
// 鼠标按下和释放
element.addEventListener('mousedown', handler); // 按下
element.addEventListener('mouseup', handler); // 释放
// 鼠标移动
element.addEventListener('mousemove', handler); // 移动
element.addEventListener('mouseenter', handler); // 进入(不冒泡)
element.addEventListener('mouseleave', handler); // 离开(不冒泡)
element.addEventListener('mouseover', handler); // 进入(冒泡)
element.addEventListener('mouseout', handler); // 离开(冒泡)
// 滚轮事件
element.addEventListener('wheel', handler);
mouseenter/mouseleave vs mouseover/mouseout:
<div id="parent">
<div id="child">子元素</div>
</div>
<script>
const parent = document.getElementById('parent');
// mouseenter/mouseleave:不会因为子元素触发
parent.addEventListener('mouseenter', () => {
console.log('进入父元素'); // 只触发一次
});
// mouseover/mouseout:会因为子元素触发
parent.addEventListener('mouseover', () => {
console.log('over父元素'); // 进入子元素时也会触发
});
</script>
# 5.2 键盘事件
// 键盘事件顺序:keydown -> keypress -> keyup
element.addEventListener('keydown', handler); // 按键按下
element.addEventListener('keypress', handler); // 产生字符(已废弃)
element.addEventListener('keyup', handler); // 按键释放
// 实际应用
document.addEventListener('keydown', function(e) {
// Esc关闭弹窗
if (e.key === 'Escape') {
closeModal();
}
// Ctrl+S保存
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
save();
}
// 方向键导航
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
navigate(e.key);
}
});
# 5.3 表单事件
const input = document.querySelector('input');
const form = document.querySelector('form');
// 输入事件
input.addEventListener('input', function(e) {
console.log('实时输入:', e.target.value);
});
// 值改变事件
input.addEventListener('change', function(e) {
console.log('值改变:', e.target.value);
});
// 焦点事件
input.addEventListener('focus', handler); // 获得焦点
input.addEventListener('blur', handler); // 失去焦点
input.addEventListener('focusin', handler); // 获得焦点(冒泡)
input.addEventListener('focusout', handler);// 失去焦点(冒泡)
// 表单提交
form.addEventListener('submit', function(e) {
e.preventDefault();
// 表单验证和提交逻辑
});
// 表单重置
form.addEventListener('reset', function(e) {
if (!confirm('确定重置表单?')) {
e.preventDefault();
}
});
# 5.4 文档和窗口事件
// 文档加载
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM加载完成');
});
window.addEventListener('load', function() {
console.log('页面完全加载(包括图片等资源)');
});
// 页面离开
window.addEventListener('beforeunload', function(e) {
e.preventDefault();
e.returnValue = ''; // 显示确认对话框
});
window.addEventListener('unload', function() {
// 页面卸载时执行清理操作
});
// 窗口大小改变
window.addEventListener('resize', function() {
console.log('窗口大小:', window.innerWidth, window.innerHeight);
});
// 滚动事件
window.addEventListener('scroll', function() {
console.log('滚动位置:', window.scrollY);
});
// 页面可见性变化
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
console.log('页面被隐藏');
pauseVideo();
} else {
console.log('页面可见');
resumeVideo();
}
});
# 5.5 拖放事件
const draggable = document.querySelector('.draggable');
const dropzone = document.querySelector('.dropzone');
// 拖动元素
draggable.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', e.target.id);
e.dataTransfer.effectAllowed = 'move';
});
draggable.addEventListener('drag', handler);
draggable.addEventListener('dragend', handler);
// 放置区域
dropzone.addEventListener('dragenter', handler);
dropzone.addEventListener('dragover', function(e) {
e.preventDefault(); // 必须阻止默认行为才能drop
});
dropzone.addEventListener('dragleave', handler);
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
const element = document.getElementById(id);
dropzone.appendChild(element);
});
# 六、自定义事件
# 6.1 创建和触发自定义事件
// 创建自定义事件
const event = new CustomEvent('myEvent', {
detail: { message: '自定义数据' },
bubbles: true,
cancelable: true
});
// 监听自定义事件
element.addEventListener('myEvent', function(e) {
console.log('自定义事件触发:', e.detail.message);
});
// 触发自定义事件
element.dispatchEvent(event);
# 6.2 组件间通信
// 组件A:发送事件
class ComponentA {
notify() {
const event = new CustomEvent('data-updated', {
detail: { data: this.data },
bubbles: true
});
this.element.dispatchEvent(event);
}
}
// 组件B:监听事件
class ComponentB {
constructor() {
document.addEventListener('data-updated', (e) => {
console.log('收到数据:', e.detail.data);
this.update(e.detail.data);
});
}
}
# 6.3 Event vs CustomEvent
// Event(基础事件)
const event1 = new Event('click', {
bubbles: true,
cancelable: true
});
// CustomEvent(可携带自定义数据)
const event2 = new CustomEvent('myEvent', {
detail: { custom: 'data' },
bubbles: true,
cancelable: true
});
# 七、综合示例
# 八、性能优化
# 8.1 事件节流(Throttle)
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// 使用示例
window.addEventListener('scroll', throttle(function() {
console.log('滚动事件');
}, 200)); // 每200ms最多执行一次
# 8.2 事件防抖(Debounce)
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用示例
const input = document.querySelector('input');
input.addEventListener('input', debounce(function(e) {
console.log('搜索:', e.target.value);
}, 300)); // 停止输入300ms后执行
# 8.3 passive事件监听器
// 提升滚动性能
document.addEventListener('touchstart', handler, { passive: true });
document.addEventListener('wheel', handler, { passive: true });
// passive: true 表示不会调用preventDefault()
// 浏览器可以立即开始滚动,不需要等待JavaScript执行
# 8.4 移除不需要的监听器
class Component {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener('click', this.handleClick);
}
handleClick(e) {
console.log('点击');
}
destroy() {
// 组件销毁时移除监听器
this.element.removeEventListener('click', this.handleClick);
}
}
# 九、最佳实践
# 9.1 事件监听
- 优先使用
addEventListener而不是onclick - 使用事件委托减少监听器数量
- 及时移除不需要的监听器
- 使用
passive优化滚动性能 - 使用节流/防抖优化高频事件
# 9.2 事件处理
- 合理使用
preventDefault()和stopPropagation() - 避免在事件处理器中执行耗时操作
- 使用
requestAnimationFrame处理动画相关事件 - 注意
this指向(使用箭头函数或bind)
# 9.3 事件委托
- 适用于大量相似元素
- 适用于动态添加的元素
- 使用
closest()查找目标元素 - 检查元素是否在容器内
# 十、总结
事件处理是JavaScript交互的核心:
- 事件监听:使用
addEventListener添加事件监听器 - 事件对象:包含事件类型、目标、位置等信息
- 事件传播:理解捕获、目标、冒泡三个阶段
- 事件委托:利用冒泡机制优化性能
- 性能优化:节流、防抖、passive、及时移除监听器
掌握事件处理机制,能够构建高性能、交互丰富的Web应用。
祝你变得更强!
编辑 (opens new window)
上次更新: 2025/11/28