02高频事件实战
真实项目里最常写的不是 click,而是
输入、滚动、缩放、提交、键盘。
这些事件「来得又快又密」,直接写业务逻辑会卡、会重复请求、会丢焦点。
本课给每个高频事件配上正确的节流 / 防抖 / 惯用法。
一、谁是「高频事件」
打字时每字符触发一次;搜索建议若每次都请求会打爆后端。
滚动一帧可触发几十次;直接改 DOM 会掉帧。
拖窗口边缘连续触发;图表重排要合并。
拖拽、画笔、tooltip 跟随,必须节流到 rAF。
按钮连点 → 提交两次;要禁用 + 防重。
滚动性能敏感;监听加 passive: true。
二、防抖 vs 节流(一图说清)
| 防抖 debounce | 节流 throttle | |
|---|---|---|
| 行为 | 停止操作 N ms 后执行一次 | 每 N ms 最多执行一次 |
| 场景 | 搜索联想、自动保存、按钮防连点 | 滚动加载、进度条、拖拽、埋点 |
| 口诀 | 「你停了我再动」 | 「你一直动我也按节拍动」 |
在上面输入…
三、scroll / resize:用 rAF 合并帧
浏览器一帧约 16ms。scroll 事件远比帧率密,直接布局读写会触发
强制同步布局(layout thrashing)。
标准做法:事件里只「记下要更新」,真正改样式丢到 requestAnimationFrame。
{ passive: true }(除非你真的要 preventDefault),
否则浏览器不敢放开合成滚动,列表会发涩。四、表单:防连点 + 校验 + 提交
提交几次?看计数
五、键盘:keydown 判断按键
e.key(语义:'Enter')而不是
e.keyCode(已废弃)。中文输入法组字阶段注意 e.isComposing。六、常见问题 · 自问自答
搜索框为什么请求打到服务器爆了?
每个字符都触发了 fetch。给 input 监听套防抖 300–500ms;
更稳的还可以用 AbortController 取消上一次未完成的请求,防止旧结果覆盖新结果(08 章讲过)。
滚动时页面很卡、掉帧怎么办?
① 监听是否写了 {passive:true};
② 回调里是否在「读布局」和「写布局」之间来回跳(offsetHeight 后立刻改 style);
③ 把写操作合并进 requestAnimationFrame;
④ 能用 IntersectionObserver 做懒加载就别用 scroll。
按钮连点导致提交两次、扣两次库存?
前端:提交瞬间 disabled = true + loading 文案;
更关键的是后端幂等(订单号去重)。前端防重只是体验,不能当唯一安全措施。
为什么 resize 里重新算布局很卡?
拖窗口时 resize 每帧都来。用节流 100–200ms 或 rAF 合并;
图表库一般自带 resizeObserver,优先用 ResizeObserver 观察容器,而不是 window.resize。
防抖里的函数 this 为什么丢了?
自己手写 debounce 时要用 fn.apply(this, args) 保存调用时的 this,
或者直接用箭头函数包业务,this 取外层。lodash 的 _.debounce 已帮你处理。
七、对照速查
| 事件 | 推荐写法 | 一坑提醒 |
|---|---|---|
| input | debounce 300–500 | 中文组字用 compositionend |
| scroll | throttle 或 rAF + passive | 勿频繁改布局 |
| resize | debounce 150 或 ResizeObserver | 拖拽窗口会连发 |
| submit | preventDefault + 禁用按钮 | 不要写在 click |
| keydown | e.key / e.metaKey | 拦 Ctrl+S 要 preventDefault |
| wheel/touch | {passive:true} | 要拦截滚动才非 passive |
八、本章小结
- 高频事件必须合并频率:防抖「停下来再做」、节流「按节拍做」。
- scroll / 触摸:加
passive: true;改样式进requestAnimationFrame。 - 表单用
submit+preventDefault,按钮防重 + 后端幂等。 - 键盘用
e.key,快捷键记得拦默认行为。