鼠标事件
鼠标事件(Mouse Events)是 Web 开发中最常用的事件类型之一,用于响应用户的鼠标操作,如点击、移动、悬浮、滚轮等。HTML 提供了一系列以 on 开头的事件属性,可以直接绑定到 HTML 元素上,也可以通过 JavaScript 的 addEventListener 方法绑定。
前置知识
阅读本节前,建议先了解:inert 非交互
基础概念
什么是鼠标事件
鼠标事件在用户使用鼠标与页面交互时触发。浏览器会创建一个 MouseEvent 对象,包含事件发生时的详细信息,如坐标位置、按键状态、目标元素等。
html
<!-- 内联事件处理器 -->
<button onclick="handleClick()">点击我</button>
<!-- HTML 事件属性 -->
<div onmouseover="this.style.backgroundColor='yellow'"
onmouseout="this.style.backgroundColor='white'">
悬浮变色
</div>鼠标事件分类
| 类别 | 事件 | 触发时机 |
|---|---|---|
| 点击 | click | 鼠标按下并释放(完整的点击) |
| 双击 | dblclick | 快速连续两次点击 |
| 按下 | mousedown | 鼠标按钮按下时 |
| 释放 | mouseup | 鼠标按钮释放时 |
| 移入 | mouseover | 鼠标进入元素(冒泡) |
| 移出 | mouseout | 鼠标离开元素(冒泡) |
| 进入 | mouseenter | 鼠标进入元素(不冒泡) |
| 离开 | mouseleave | 鼠标离开元素(不冒泡) |
| 移动 | mousemove | 鼠标在元素上移动时 |
| 滚轮 | wheel | 鼠标滚轮滚动时 |
| 右键 | contextmenu | 右键点击时 |
语法
HTML 事件属性
html
<button onclick="alert('被点击了')">点击</button>
<div ondblclick="this.style.color='red'">双击变色</div>
<input onmousedown="this.value='按下'" onmouseup="this.value='释放'">
<div onmousemove="console.log('移动中...')">移动区域</div>
<div onwheel="console.log('滚轮:', event.deltaY)">滚动区域</div>
<div oncontextmenu="event.preventDefault()">禁用右键菜单</div>JavaScript addEventListener
javascript
const btn = document.getElementById('my-btn');
// 单次点击
btn.addEventListener('click', (event) => {
console.log('坐标:', event.clientX, event.clientY);
});
// 双击
btn.addEventListener('dblclick', () => {
console.log('双击触发');
});
// 鼠标按下/释放
btn.addEventListener('mousedown', (e) => {
console.log('按下:', e.button); // 0=左键 1=中键 2=右键
});
btn.addEventListener('mouseup', () => {
console.log('释放');
});详细说明
MouseEvent 对象属性
MouseEvent 对象提供了丰富的属性来获取鼠标事件信息:
| 属性 | 类型 | 说明 |
|---|---|---|
clientX / clientY | number | 相对于视口的坐标 |
pageX / pageY | number | 相对于文档的坐标(含滚动) |
screenX / screenY | number | 相对于屏幕的坐标 |
offsetX / offsetY | number | 相对于目标元素的坐标 |
button | number | 0=左键 1=中键 2=右键 3=后退 4=前进 |
buttons | number | 当前按下的所有按键(位掩码) |
altKey / ctrlKey / shiftKey / metaKey | boolean | 修饰键是否按下 |
target | Element | 触发事件的元素 |
relatedTarget | Element | 相关元素(移入/移出时) |
detail | number | click 事件的连续点击次数 |
mouseover/mouseout vs mouseenter/mouseleave
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>鼠标事件冒泡对比</title>
<style>
.outer {
width: 300px;
height: 300px;
background: #dbeafe;
padding: 40px;
border: 2px solid #3b82f6;
}
.inner {
width: 200px;
height: 200px;
background: #93c5fd;
display: flex;
align-items: center;
justify-content: center;
}
.log {
margin-top: 16px;
padding: 12px;
background: #f1f5f9;
border-radius: 8px;
font-family: monospace;
font-size: 13px;
max-height: 200px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="outer" id="outer">
外层容器
<div class="inner" id="inner">内层容器</div>
</div>
<div class="log" id="log"></div>
<script>
const log = document.getElementById('log');
function addLog(msg) {
log.innerHTML += msg + '<br>';
log.scrollTop = log.scrollHeight;
}
// mouseover/mouseout:冒泡,子元素进入/离开也会触发
document.getElementById('outer').addEventListener('mouseover', (e) => {
addLog(`mouseover → 目标: ${e.target.id || e.target.tagName}`);
});
document.getElementById('outer').addEventListener('mouseout', (e) => {
addLog(`mouseout → 目标: ${e.target.id || e.target.tagName}`);
});
// mouseenter/mouseleave:不冒泡,只在元素边界触发
document.getElementById('outer').addEventListener('mouseenter', () => {
addLog('mouseenter → 外层');
});
document.getElementById('outer').addEventListener('mouseleave', () => {
addLog('mouseleave → 外层');
});
</script>
</body>
</html>buttons 位掩码
buttons 属性使用位掩码表示当前按下的所有鼠标按键:
javascript
element.addEventListener('mousemove', (e) => {
if (e.buttons & 1) console.log('左键按下');
if (e.buttons & 2) console.log('右键按下');
if (e.buttons & 4) console.log('中键按下');
});
// buttons 位掩码值
// 1 - 左键
// 2 - 右键
// 4 - 中键
// 8 - 后退按钮
// 16 - 前进按钮滚轮事件(wheel)
javascript
const container = document.getElementById('scroll-container');
container.addEventListener('wheel', (e) => {
e.preventDefault(); // 阻止默认滚动
// deltaY > 0 表示向下滚动,< 0 表示向上滚动
if (e.deltaY > 0) {
container.scrollLeft += 100;
} else {
container.scrollLeft -= 100;
}
// deltaMode: 0=像素, 1=行, 2=页
console.log('deltaY:', e.deltaY, '模式:', e.deltaMode);
}, { passive: false });实战示例
完整的拖拽移动示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>鼠标拖拽示例</title>
<style>
.draggable {
width: 120px;
height: 120px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 12px;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
user-select: none;
position: absolute;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: box-shadow 0.2s;
}
.draggable:active {
cursor: grabbing;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.drop-zone {
width: 200px;
height: 200px;
border: 2px dashed #94a3b8;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
margin-top: 200px;
margin-left: 300px;
}
.drop-zone.hover {
border-color: #3b82f6;
background: #eff6ff;
color: #3b82f6;
}
.coords {
position: fixed;
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 16px;
border-radius: 8px;
font-family: monospace;
font-size: 13px;
}
</style>
</head>
<body>
<div class="draggable" id="box">拖拽我</div>
<div class="drop-zone" id="zone">放置区域</div>
<div class="coords" id="coords">x: 0, y: 0</div>
<script>
const box = document.getElementById('box');
const zone = document.getElementById('zone');
const coords = document.getElementById('coords');
let isDragging = false;
let offsetX, offsetY;
// 鼠标按下:开始拖拽
box.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - box.offsetLeft;
offsetY = e.clientY - box.offsetTop;
box.style.cursor = 'grabbing';
e.preventDefault(); // 防止选中文字
});
// 鼠标移动:拖拽中
document.addEventListener('mousemove', (e) => {
// 更新坐标显示
coords.textContent = `x: ${e.clientX}, y: ${e.clientY}`;
if (!isDragging) return;
box.style.left = (e.clientX - offsetX) + 'px';
box.style.top = (e.clientY - offsetY) + 'px';
// 检测是否在放置区域上方
const zoneRect = zone.getBoundingClientRect();
if (e.clientX >= zoneRect.left && e.clientX <= zoneRect.right &&
e.clientY >= zoneRect.top && e.clientY <= zoneRect.bottom) {
zone.classList.add('hover');
} else {
zone.classList.remove('hover');
}
});
// 鼠标释放:结束拖拽
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
box.style.cursor = 'grab';
// 检测是否放置在区域内
if (zone.classList.contains('hover')) {
zone.textContent = '已放置!';
zone.style.borderColor = '#22c55e';
zone.style.background = '#f0fdf4';
zone.style.color = '#22c55e';
box.style.display = 'none';
}
});
</script>
</body>
</html>注意事项
性能优化
mousemove 事件触发频率极高,需要注意性能:
javascript
// 使用节流(throttle)减少处理频率
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}
// 推荐:使用 requestAnimationFrame
let animationId = null;
element.addEventListener('mousemove', (e) => {
if (!animationId) {
animationId = requestAnimationFrame(() => {
// 处理鼠标移动
animationId = null;
});
}
});触摸设备兼容
在触摸设备上,鼠标事件可能不会触发。应同时监听触摸事件:
javascript
// 同时支持鼠标和触摸
element.addEventListener('mousedown', handleClick);
element.addEventListener('touchstart', handleClick, { passive: true });
// 或使用 Pointer Events API(统一处理)
element.addEventListener('pointerdown', handlePointerDown);
element.addEventListener('pointermove', handlePointerMove);
element.addEventListener('pointerup', handlePointerUp);阻止默认行为
html
<!-- 阻止右键菜单 -->
<div oncontextmenu="event.preventDefault(); showCustomMenu()">
右键点击显示自定义菜单
</div>
<!-- 阻止图片拖拽 -->
<img src="photo.jpg" draggable="false"
onmousedown="event.preventDefault()">最佳实践
1. 使用 event delegation 减少事件监听器
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>事件委托示例</title>
</head>
<body>
<ul id="item-list">
<li data-id="1">项目一</li>
<li data-id="2">项目二</li>
<li data-id="3">项目三</li>
<!-- 可能有数百个列表项 -->
</ul>
<script>
// 推荐:在父元素上使用事件委托
document.getElementById('item-list').addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return;
const id = li.dataset.id;
console.log('点击了项目:', id);
});
// 不推荐:为每个列表项单独绑定
// document.querySelectorAll('#item-list li').forEach(li => {
// li.addEventListener('click', () => { ... });
// });
</script>
</body>
</html>2. 区分单击和双击
javascript
let clickCount = 0;
let clickTimer = null;
element.addEventListener('click', () => {
clickCount++;
if (clickCount === 1) {
clickTimer = setTimeout(() => {
clickCount = 0;
console.log('单击');
}, 300);
} else if (clickCount === 2) {
clearTimeout(clickTimer);
clickCount = 0;
console.log('双击');
}
});下一节
继续学习:键盘事件