Skip to content

鼠标事件

鼠标事件(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 / clientYnumber相对于视口的坐标
pageX / pageYnumber相对于文档的坐标(含滚动)
screenX / screenYnumber相对于屏幕的坐标
offsetX / offsetYnumber相对于目标元素的坐标
buttonnumber0=左键 1=中键 2=右键 3=后退 4=前进
buttonsnumber当前按下的所有按键(位掩码)
altKey / ctrlKey / shiftKey / metaKeyboolean修饰键是否按下
targetElement触发事件的元素
relatedTargetElement相关元素(移入/移出时)
detailnumberclick 事件的连续点击次数

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('双击');
  }
});

下一节

继续学习:键盘事件

参考链接