2、UI事件类型:鼠标事件、键盘事件、触摸事件、窗口事件、自定义事件

事件类型,说白了就是用户与界面交互的「语言」。不同的设备、不同的操作,对应着不同的事件类型。我刚开始做UI开发时,总觉得事件类型背下来就行,后来踩了坑才明白——理解每种事件的触发时机和特性,比死记硬背重要得多。

咱们把UI事件分成五大类来聊:鼠标、键盘、触摸、窗口,还有自定义事件。每一类都有自己的脾气。

2.1 鼠标事件

鼠标事件是最经典的UI事件类型。从桌面端到Web端,鼠标事件几乎无处不在。我个人习惯把鼠标事件分成三个层级:

层级 事件 触发时机
基础 click / dblclick 单击/双击
移动 mousedown / mousemove / mouseup 按下/移动/释放
进出 mouseenter / mouseleave / mouseover / mouseout 进入/离开区域

这里有个坑,我经常看到新人搞混 mouseenter 和 mouseover。简单说:mouseenter 不冒泡,mouseover 冒泡。如果你在父元素上监听 mouseover,子元素进出也会触发,这往往不是你想要的。

我曾经踩过的坑: 在拖拽功能里同时监听 mousemove 和 mouseup,结果鼠标移出浏览器窗口后事件丢失。解决方案是监听 document 级别的 mouseup,或者在窗口外捕获鼠标事件。
// 鼠标事件监听示例
element.addEventListener('mousedown', (e) => {
  console.log('鼠标按下', e.clientX, e.clientY);
});

element.addEventListener('mouseup', (e) => {
  console.log('鼠标释放', e.clientX, e.clientY);
});

2.2 键盘事件

键盘事件处理的是按键输入。你想想看,用户按下一个键,其实经历了三个阶段:

  • keydown:按键按下瞬间触发
  • keypress:字符输入时触发(已废弃,但老项目里还能见到)
  • keyup:按键释放时触发

我建议你重点关注 keydown 和 keyup。为什么?因为 keypress 无法捕获功能键(比如 Ctrl、Shift、方向键),而且不同浏览器实现有差异。

小技巧: 判断组合键时,用 e.ctrlKey、e.shiftKey、e.altKey 这些属性,比自己去解析 keyCode 靠谱得多。
document.addEventListener('keydown', (e) => {
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault(); // 阻止浏览器默认保存行为
    console.log('触发自定义保存逻辑');
  }
});

嗯,这里要注意:e.key 返回的是按键的字符值,比如 'a'、'Enter';而 e.code 返回的是物理按键位置,比如 'KeyA'、'Enter'。两者区别很大,我曾在国际键盘布局下吃过亏——用户按了不同位置的键,e.key 一样但 e.code 不同。

2.3 触摸事件

移动端时代,触摸事件是必修课。触摸事件的核心是「多点触控」,这和鼠标事件有本质区别。

事件 触发时机 关键属性
touchstart 手指触摸屏幕 touches / targetTouches
touchmove 手指在屏幕滑动 changedTouches
touchend 手指离开屏幕 changedTouches
touchcancel 系统中断触摸 —

我建议你记住一个原则:触摸事件和鼠标事件不要混用。移动端浏览器会同时触发触摸事件和鼠标事件(为了兼容),如果你两个都监听,逻辑会执行两次。

避坑指南: 我曾经在移动端实现滑动删除功能,同时监听了 touchmove 和 mousemove。结果在 Chrome 模拟器上测试没问题,真机上滑动时触发了两次删除逻辑。解决方案是判断事件类型,或者用 e.preventDefault() 阻止鼠标事件的触发。
element.addEventListener('touchstart', (e) => {
  const touch = e.touches[0];
  console.log('触摸位置', touch.clientX, touch.clientY);
});

element.addEventListener('touchmove', (e) => {
  e.preventDefault(); // 阻止页面滚动
  const touch = e.touches[0];
  // 处理滑动逻辑
});

2.4 窗口事件

窗口事件和浏览器窗口本身相关,不依赖具体的UI元素。这类事件往往被忽视,但实际项目中非常关键。

  • resize:窗口大小变化
  • scroll:页面滚动
  • load:页面加载完成
  • beforeunload:页面即将关闭
  • visibilitychange:页面可见性变化

我个人觉得 visibilitychange 是窗口事件里最实用的。为什么?因为用户切换标签页时,你的应用可能还在跑动画、发请求,白白浪费资源。

优化建议: 在 visibilitychange 事件里暂停视频播放、停止轮询请求、暂停动画循环。页面重新可见时再恢复。这个优化能让你的应用更省电、更流畅。
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    console.log('页面不可见,暂停资源消耗');
    // 暂停动画、停止轮询
  } else {
    console.log('页面可见,恢复操作');
    // 恢复动画、重新请求数据
  }
});

还有 resize 事件,我建议做防抖处理。窗口变化时事件触发频率极高,不做防抖的话,性能会受影响。

2.5 自定义事件

内置事件不够用怎么办?自己造。自定义事件是UI事件驱动架构的「粘合剂」,它让组件之间可以解耦通信。

创建自定义事件有两种方式:

  1. 使用 Event 构造函数:简单场景
  2. 使用 CustomEvent 构造函数:需要传递数据时
// 方式一:简单事件
const event = new Event('userLogin');
document.dispatchEvent(event);

// 方式二:带数据的事件
const customEvent = new CustomEvent('dataLoaded', {
  detail: { userId: 123, name: '张三' }
});
document.dispatchEvent(customEvent);

// 监听自定义事件
document.addEventListener('dataLoaded', (e) => {
  console.log('数据加载完成', e.detail);
});
注意: 自定义事件默认不冒泡。如果你需要事件冒泡,在构造函数里设置 bubbles: true。我遇到过组件嵌套时事件传不上去的问题,就是因为忘了设置冒泡。

自定义事件的好处是:你可以在任何地方触发,在任何地方监听。说白了,它就是一个全局的消息总线。但别滥用,否则事件满天飞,调试起来很痛苦。

2.6 事件类型关系图

下面这张图展示了五种事件类型的关系和典型应用场景:

UI事件类型关系图 UI事件驱动 鼠标事件 键盘事件 触摸事件 窗口事件 自定义事件 说明: 鼠标、键盘、触摸、窗口是浏览器内置事件类型,各自对应不同的交互方式。 自定义事件是开发者自己定义的事件,用于组件间解耦通信。 实际开发中,这五类事件经常组合使用,比如移动端同时处理触摸事件和窗口事件。

2.7 事件类型选择建议

说了这么多,到底该用哪种事件?我总结了几条经验:

场景 推荐事件类型 原因
桌面端点击按钮 click 简单直接,兼容性好
拖拽功能 mousedown + mousemove + mouseup 需要精确控制按下、移动、释放
快捷键操作 keydown 能捕获组合键,响应快
移动端滑动 touchstart + touchmove + touchend 原生触摸支持,性能好
页面可见性变化 visibilitychange 比 focus/blur 更准确
组件间通信 自定义事件 解耦,灵活
核心原则: 能用内置事件解决的问题,不要用自定义事件。内置事件经过浏览器优化,性能和兼容性都更好。自定义事件适合「跨组件、跨层级」的通信场景。