前端-JavaScript 事件大全 & 速查清单
一文吃透 JS 事件:模型、阶段、注册方式、常见事件、事件对象、冒泡/捕获、阻止默认、事件委托、性能与最佳实践、以及自定义事件。
目录
- 事件模型与术语
- 事件注册方式
- 事件传播:捕获 → 目标 → 冒泡
- 阻止默认与终止传播
- 事件对象 Event 常用属性
- 常见事件类型速查
- 事件委托(高性能必备)
- addEventListener 选项
- 自定义事件 CustomEvent
- 指针事件 vs 鼠标/触摸事件
- 键盘与输入法细节
- 表单事件小贴士
- 移除事件监听的坑
- Shadow DOM 与 composedPath
- 性能优化与最佳实践清单
- FAQ:常见疑惑
- 示例汇总
事件模型与术语
- 事件(Event):浏览器在某个时刻/行为发生时发出的“信号”,如
click
、keydown
、scroll
。 - 事件目标(target):事件最初发生的那个元素。
- 当前目标(currentTarget):当前触发回调的那个元素(总等于绑定监听器的元素)。
- 传播(Propagation):事件沿 DOM 树传播的过程:捕获 → 目标 → 冒泡。
- 默认行为(Default Action):浏览器对某些事件的内置处理,比如
<a>
的跳转、表单提交、滚动等。
事件注册方式
1) HTML 内联(不推荐)
<button onclick="alert('hi')">Click</button>
- 缺点:逻辑污染 HTML、只有冒泡阶段、难维护。
2) DOM Level 0(老方式,不冒泡参数不可选)
btn.onclick = function(e) { /* ... */ }
btn.onclick = null // 移除
3) DOM Level 2(推荐)
function handle(e){ /* ... */ }
btn.addEventListener('click', handle, /* options */)
btn.removeEventListener('click', handle)
- 支持 捕获/冒泡、
once/passive/signal
等高级选项。
事件传播:捕获 → 目标 → 冒泡
!> 顺序:window → document → html → body → … → 目标(target) → … → body → html → document → window
parent.addEventListener('click', () => console.log('捕获 parent'), true) // capture
child.addEventListener('click', () => console.log('目标 child')) // at target
parent.addEventListener('click', () => console.log('冒泡 parent')) // bubble
- 第三个参数为
true
表示 捕获;默认是 冒泡。 - 大多数事件是冒泡的,极少数不冒泡(如
blur
,focus
,mouseenter
,mouseleave
)。
阻止默认与终止传播
el.addEventListener('click', (e) => {
e.preventDefault() // 阻止默认行为(如链接跳转、表单提交、滚动)
e.stopPropagation() // 阻止继续冒泡(或捕获)到更外层
e.stopImmediatePropagation() // 还会阻止当前元素上后续监听器
}, { passive: false })
passive: true
时对“可滚动”类事件(touchstart/move
,wheel
)无法preventDefault()
。
事件对象 Event 常用属性
属性 | 含义 | 备注 |
---|---|---|
type |
事件类型 | 如 'click' |
target |
事件最初触发元素 | 可能是深层子节点 |
currentTarget |
正在处理监听器的元素 | 永远等于绑定元素 |
eventPhase |
1 捕获 / 2 目标 / 3 冒泡 | 调试传播阶段 |
timeStamp |
事件时间戳 | 单位毫秒 |
defaultPrevented |
是否已调用 preventDefault() |
布尔 |
composedPath() |
真实传播路径 | 含 Shadow DOM |
isTrusted |
用户触发还是脚本触发 | 只读 |
鼠标/指针相关
clientX/Y
, pageX/Y
, screenX/Y
, button
(0左1中2右), buttons
, altKey/ctrlKey/shiftKey/metaKey
。
键盘相关
key
(如 'a'
, 'Enter'
), code
(物理键位,如 KeyA
), repeat
。
常见事件类型速查
鼠标类
click
, dblclick
, mousedown
, mouseup
, mousemove
, contextmenu
, mouseenter/leave
(不冒泡), mouseover/out
(冒泡)。
指针类(推荐统一输入)
pointerdown/up/move/cancel/enter/leave/over/out
, gotpointercapture/lostpointercapture
。
触摸类(移动端旧接口)
touchstart/move/end/cancel
(更建议用 Pointer Events 统一处理)。
键盘类
keydown
, keyup
(无 keypress
了)。
表单类
input
, change
, focus/blur
(不冒泡,使用 focusin/focusout
代替可冒泡版)。
其他
submit
, reset
, scroll
, wheel
, resize
, DOMContentLoaded
, load
, beforeunload
, visibilitychange
等。
📌 常见事件类型
类别 | 事件 |
---|---|
鼠标 | click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave |
键盘 | keydown, keyup, keypress (废弃) |
表单 | input, change, submit, focus, blur |
触摸 | touchstart, touchmove, touchend, touchcancel |
指针 | pointerdown, pointerup, pointermove, pointercancel |
页面 | load, unload, resize, scroll, visibilitychange |
剪贴板 | copy, cut, paste |
拖放 | dragstart, dragover, drop, dragend |
自定义事件 | new CustomEvent(type, options) + dispatchEvent |
事件委托(高性能必备)
把子元素的事件监听“交给”父元素统一处理,减少监听器数量。
<ul id="list">
<li data-id="1">One</li>
<li data-id="2">Two</li>
</ul>
<script>
const list = document.getElementById('list')
list.addEventListener('click', (e) => {
const li = e.target.closest('li') // 命中近邻 li
if (!li || !list.contains(li)) return
console.log('点击了 li#', li.dataset.id)
})
</script>
- 利用 冒泡 +
closest()
命中实际目标。 - 动态新增的子节点也天然生效,适合长列表。
addEventListener 选项
el.addEventListener('touchmove', onMove, {
capture: false, // 是否在捕获阶段触发
once: true, // 回调执行一次后自动移除
passive: true, // 告诉浏览器“不会调用 preventDefault” → 滚动更流畅
signal: abortCtrl.signal // 可用 AbortController 统一移除
})
// 统一移除
abortCtrl.abort()
自定义事件 CustomEvent
const evt = new CustomEvent('cart:add', {
detail: { id: 123, qty: 2 },
bubbles: true,
composed: true // 允许穿越 Shadow DOM 边界
})
document.querySelector('#add').dispatchEvent(evt)
document.addEventListener('cart:add', (e) => {
console.log('添加到购物车:', e.detail)
})
- 业务中可用命名空间风格:
'cart:add'
,'user:login'
等。
指针事件 vs 鼠标/触摸事件
- Pointer Events 统一处理鼠标、触摸、手写笔:更现代,建议优先。
- 支持压力/倾角等扩展属性(
pressure
,tiltX/Y
,pointerType
)。 - 旧代码需要兼容时:同时监听鼠标/触摸或做特性检测。
键盘与输入法细节
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.isComposing) { /* 提交 */ }
})
// 输入法(中文拼音/日文等)
input.addEventListener('compositionstart', () => {})
input.addEventListener('compositionend', () => {})
- 输入法合成期内
keydown
可能多次触发但不应提交,需判定isComposing
。
表单事件小贴士
input
:每次内容变动就触发;change
:失焦或回车才触发(取决于控件)。focus/blur
不冒泡,若需要委托用focusin/focusout
。- 阻止表单默认提交:
form.addEventListener('submit', e => e.preventDefault())
。
移除事件监听的坑
removeEventListener
必须传入与addEventListener
同一个函数引用与同一组 options(capture 值)。
el.addEventListener('click', () => {}) // ❌ 匿名函数无法移除
const fn = () => {}
el.addEventListener('click', fn, true)
el.removeEventListener('click', fn, true) // ✅ capture 必须一致
Shadow DOM 与 composedPath
el.addEventListener('click', (e) => {
const path = e.composedPath() // 最真实的传播路径(含 Shadow 边界)
console.log(path.map(n => n.nodeName).join(' → '))
})
- 在 Web Components 中判断命中元素要优先考虑
composedPath()
。
性能优化与最佳实践清单
- ✅ 事件委托:长列表/频繁增删节点时优先。
- ✅ passive:
wheel
/touchstart
/touchmove
等尽量passive: true
。 - ✅ 节流/防抖:
scroll
/resize
/mousemove
高频事件配合requestAnimationFrame
或节流。 - ✅ 一次性监听:能
once
就once
,避免遗留。 - ✅ 统一移除:
AbortController
批量注销。 - ✅ 减少匿名函数,便于移除与复用。
- ✅ 避免阻塞主线程:重活放到 Web Worker 或分片执行。
FAQ:常见疑惑
Q1:target
与 currentTarget
有啥区别?
target
是“谁被点了”;currentTarget
是“谁在处理监听”。委托里两者常不同。
Q2:为什么 preventDefault
没效果?
- 你可能用了
passive: true
(不可阻止),或事件本身无默认行为。
Q3:stopPropagation
和 stopImmediatePropagation
区别?
- 前者阻止继续传播到父级;后者还会阻止同一元素上后续监听器。
Q4:哪些事件不冒泡?
- 常见如
blur
,focus
,mouseenter
,mouseleave
。用可冒泡替代:focusin
/focusout
、mouseover
/mouseout
。
示例汇总
节流滚动 + passive
let ticking = false
window.addEventListener('scroll', (e) => {
if (!ticking) {
requestAnimationFrame(() => {
// 处理滚动逻辑...
ticking = false
})
ticking = true
}
}, { passive: true })
统一移除(AbortController)
const ac = new AbortController()
const opts = { signal: ac.signal }
window.addEventListener('resize', onResize, opts)
document.addEventListener('visibilitychange', onVis, opts)
// ...更多监听
ac.abort() // 一键全部移除
组合:委托 + closest + 自定义事件
const ac = new AbortController()
document.body.addEventListener('click', (e) => {
const btn = e.target.closest('[data-add]')
if (!btn) return
btn.dispatchEvent(new CustomEvent('cart:add', {
detail: { id: btn.dataset.add, qty: 1 }, bubbles: true
}))
}, { signal: ac.signal })
document.addEventListener('cart:add', (e) => {
console.log('添加:', e.detail)
})
小结:事件不是“点一下那么简单”。理解 传播阶段、善用 委托、配合 passive/once/signal,加点 CustomEvent 的业务语义,你的交互会更丝滑,代码也更优雅。继续冲~🚀