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 事件类型全景图
说了这么多,咱们用一张图把五类事件的关系串起来。这张图我画了很多次,每次给团队新人讲事件机制,都会先让他们看这个。
这张图把五类事件的关系和子事件都列出来了。你想想看,从中心的事件系统出发,分支出五大类,每类又有自己的子事件。理解了这张图,你就掌握了事件分类的骨架。
2.7 如何选择合适的事件类型
说了这么多,到底该用哪个?我总结了一个简单的选择逻辑:
- 用户用鼠标操作? → 鼠标事件(click、mousedown 等)
- 用户用键盘操作? → 键盘事件(keydown、keyup)
- 用户用手指操作? → 触控事件(touchstart、touchmove、touchend)
- 窗口或页面状态变化? → 窗口事件(resize、scroll、load)
- 业务逻辑需要通知其他模块? → 自定义事件
这个逻辑看起来简单,但实际项目中经常有人搞混。我记得有一次,一个同事用 click 事件做移动端的按钮交互,结果在 iOS 上总是有延迟。后来改成 touchstart,体验立马好了。说白了,选对事件类型,比写好事件处理逻辑更重要。
我的建议:做跨平台项目时,可以封装一个事件适配层。比如检测到触控设备就用触控事件,检测到鼠标设备就用鼠标事件。这样代码更干净,也更容易维护。