2. 事件类型与分类:鼠标事件、键盘事件、触控事件、窗口事件、自定义事件

事件类型,说白了就是用户与界面交互的「语言」。你想想看,用户点一下、敲个键、划个屏,系统怎么知道该干嘛?靠的就是事件类型来区分。我做了这么多年 UI,见过太多人把事件类型搞混,结果界面要么没反应,要么乱跳。今天咱们就把这五类事件彻底捋清楚。

核心观点:事件类型决定了「谁触发了什么」。鼠标、键盘、触控、窗口、自定义——这五类覆盖了 99% 的交互场景。理解它们的差异,是写出健壮 UI 的第一步。

2.1 鼠标事件:最经典的交互方式

鼠标事件,大家最熟悉了。从早期的桌面软件到现在的 Web 应用,鼠标一直是主力。我个人习惯把鼠标事件分成三组:点击类、移动类、滚轮类。

事件名 触发时机 常用场景
click 鼠标按下并释放 按钮点击、链接跳转
dblclick 双击 编辑文本、放大视图
mousedown 鼠标按下瞬间 拖拽开始、绘画工具
mouseup 鼠标释放瞬间 拖拽结束、选择完成
mousemove 鼠标移动 悬停效果、实时坐标
mouseenter 鼠标进入元素 菜单展开、提示框
mouseleave 鼠标离开元素 菜单收起、提示消失
wheel 滚轮滚动 页面滚动、缩放

这里有个坑,我踩过好几次。很多人以为 click 就是 mousedown + mouseup,其实不完全对。click 事件要求鼠标在同一个元素上按下并释放,如果按下后拖到别处再释放,click 不会触发,但 mousedown 和 mouseup 都会触发。我曾经做一个拖拽排序组件,就是因为没区分这两个事件,导致用户拖到一半松手,排序逻辑乱掉了。

小技巧:需要精确控制拖拽行为时,用 mousedown 开始、mousemove 跟踪、mouseup 结束。别依赖 click 做拖拽逻辑。

2.2 键盘事件:从按键到命令

键盘事件,说白了就是用户按了哪个键。我建议你记住三个核心事件:keydown、keypress、keyup。不过现在 keypress 已经被废弃了,主流都用 keydown 和 keyup。

// 监听键盘事件
document.addEventListener('keydown', function(event) {
  console.log('按下的键:', event.key);
  console.log('键码:', event.code);
  console.log('是否按了 Ctrl:', event.ctrlKey);
  console.log('是否按了 Shift:', event.shiftKey);
});

为什么 keypress 被废弃了?因为它只能捕获可打印字符,像 Ctrl、Shift、F1 这些功能键它根本抓不到。而 keydown 能捕获所有按键,包括组合键。我记得有一次做快捷键系统,用了 keypress 监听 Ctrl+S,结果死活不触发——后来才发现 Ctrl 本身就不触发 keypress。

注意:keydown 事件会重复触发。如果你按住一个键不放,它会连续触发多次。做游戏或连续操作时要注意防抖,不然逻辑会炸。

2.3 触控事件:移动端的核心

触控事件,移动端的命根子。你想想看,手机上没有鼠标,没有键盘,全靠手指划来划去。触控事件的核心就三个:touchstart、touchmove、touchend。

但触控有个大坑——它支持多点触控。一个手指叫一个 touch 对象,多个手指就是多个 touch 对象。我建议你通过 event.touches 获取所有触摸点,event.changedTouches 获取变化的触摸点。

// 实现双指缩放
element.addEventListener('touchstart', function(event) {
  if (event.touches.length === 2) {
    // 记录两个手指的初始距离
    const dx = event.touches[0].clientX - event.touches[1].clientX;
    const dy = event.touches[0].clientY - event.touches[1].clientY;
    this.startDistance = Math.sqrt(dx * dx + dy * dy);
  }
});

element.addEventListener('touchmove', function(event) {
  if (event.touches.length === 2) {
    const dx = event.touches[0].clientX - event.touches[1].clientX;
    const dy = event.touches[0].clientY - event.touches[1].clientY;
    const currentDistance = Math.sqrt(dx * dx + dy * dy);
    const scale = currentDistance / this.startDistance;
    // 应用缩放
    this.style.transform = `scale(${scale})`;
  }
});

我曾经做一个图片查看器,用户反馈双指缩放时图片会「跳一下」。排查了半天,发现是 touchstart 里没判断手指数量,单指滑动也被当成缩放处理了。嗯,这里要注意:一定要先检查 event.touches.length,再决定是滑动还是缩放。

关键区别:鼠标事件和触控事件不能混用。触控设备上,鼠标事件也会触发(为了兼容),但会有 300ms 延迟。如果你要做移动端,优先用触控事件,别偷懒用 click。

2.4 窗口事件:页面级别的响应

窗口事件,说白了就是浏览器窗口本身发生了什么。比如用户调整窗口大小、滚动页面、切换标签页。这些事件不针对某个元素,而是针对整个 window 对象。

事件名 触发时机 典型用途
resize 窗口大小变化 响应式布局调整
scroll 页面滚动 懒加载、无限滚动
load 页面完全加载 初始化资源
beforeunload 页面即将关闭 提示保存未提交数据
hashchange URL 哈希变化 单页应用路由

窗口事件有个特点:触发频率可能非常高。比如 scroll 和 resize,用户滚一下鼠标,可能触发几十次。我建议你加上节流(throttle)或防抖(debounce),不然性能会崩。

// 防抖处理 resize 事件
let resizeTimer;
window.addEventListener('resize', function() {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(() => {
    console.log('窗口大小已稳定,执行布局调整');
    // 你的逻辑
  }, 200);
});

个人习惯:我一般用 requestAnimationFrame 配合 scroll 事件,比 setTimeout 更平滑,而且不会掉帧。

2.5 自定义事件:让组件自己说话

自定义事件,这是我最喜欢的一类。你想想看,系统提供的事件就那么多,但你的业务逻辑千奇百怪。比如「用户完成了新手引导」、「购物车商品数量变了」——这些系统事件根本没法表达。怎么办?自己造一个。

在 JavaScript 里,用 CustomEvent 构造函数就能创建自定义事件。我建议你给事件名加个命名空间,比如 app:cart-update,避免跟系统事件冲突。

// 创建自定义事件
const cartUpdateEvent = new CustomEvent('app:cart-update', {
  detail: {
    itemCount: 3,
    totalPrice: 299.99
  },
  bubbles: true,    // 允许冒泡
  cancelable: true  // 允许取消
});

// 触发事件
document.dispatchEvent(cartUpdateEvent);

// 监听事件
document.addEventListener('app:cart-update', function(event) {
  console.log('购物车更新了:', event.detail);
});

自定义事件的好处是解耦。比如你的购物车组件更新了数据,不需要直接调用其他组件的方法,只需要触发一个事件,谁关心谁去监听。我曾经在一个大型后台项目中,用自定义事件把十几个模块串起来,代码清晰多了。

避坑指南:自定义事件默认不会冒泡。如果你想让父元素也能收到,记得设置 bubbles: true。另外,detail 属性里可以放任意数据,但别放太复杂的东西,保持简单。

2.6 事件类型全景图

说了这么多,咱们用一张图把五类事件的关系串起来。这张图我画了很多次,每次给团队新人讲事件机制,都会先让他们看这个。

事件类型全景图 UI 事件系统 鼠标事件 键盘事件 触控事件 窗口事件 自定义事件 子事件 click / dblclick mousedown / mouseup mousemove / mouseenter mouseleave / wheel 子事件 keydown / keyup keypress(已废弃) 子事件 touchstart / touchmove touchend / touchcancel 多点触控支持 子事件 resize / scroll load / beforeunload 子事件 CustomEvent dispatchEvent 核心要点 • 鼠标事件:最经典,注意 click 与 mousedown/mouseup 的区别 • 键盘事件:用 keydown 替代已废弃的 keypress,注意组合键 • 触控事件:移动端优先,注意多点触控和 300ms 延迟

这张图把五类事件的关系和子事件都列出来了。你想想看,从中心的事件系统出发,分支出五大类,每类又有自己的子事件。理解了这张图,你就掌握了事件分类的骨架。

2.7 如何选择合适的事件类型

说了这么多,到底该用哪个?我总结了一个简单的选择逻辑:

  1. 用户用鼠标操作? → 鼠标事件(click、mousedown 等)
  2. 用户用键盘操作? → 键盘事件(keydown、keyup)
  3. 用户用手指操作? → 触控事件(touchstart、touchmove、touchend)
  4. 窗口或页面状态变化? → 窗口事件(resize、scroll、load)
  5. 业务逻辑需要通知其他模块? → 自定义事件

这个逻辑看起来简单,但实际项目中经常有人搞混。我记得有一次,一个同事用 click 事件做移动端的按钮交互,结果在 iOS 上总是有延迟。后来改成 touchstart,体验立马好了。说白了,选对事件类型,比写好事件处理逻辑更重要。

我的建议:做跨平台项目时,可以封装一个事件适配层。比如检测到触控设备就用触控事件,检测到鼠标设备就用鼠标事件。这样代码更干净,也更容易维护。