事件类型
Web 平台事件体系非常庞杂,除了 DOM Level 3 Events 定义的标准事件,HTML 规范、浏览器扩展以及多端设备也带来了更多补充。为了在项目中选择合适的事件,需要理解它们的分类、触发时机以及常用属性。
[[TOC]]
事件分类速览
| 分类 | 典型场景 | 常见事件举例 |
|---|---|---|
| UI(用户界面) | 页面结构/资源加载、滚动、尺寸变化 | load、DOMContentLoaded、resize、scroll |
| 焦点 | 元素获得/失去焦点、可访问性提示 | focus、blur、focusin、focusout |
| 鼠标与滚轮 | 光标移动、按钮点击、滚轮滑动 | click、mousedown、mousemove、wheel |
| 键盘与输入 | 键盘交互、文本输入、剪贴板、IME | keydown、keyup、input、compositionend |
| 指针事件 | 鼠标、触摸、笔统一输入 | pointerdown、pointermove、pointercancel |
| 拖放 | 元素拖拽、文件拖放 | dragstart、dragover、drop |
| 表单 | 表单提交、重置、值变化 | submit、reset、change、input |
| 剪贴板 | 复制、剪切、粘贴 | copy、cut、paste |
| 多媒体 | 音视频播放控制 | play、pause、ended、timeupdate |
| 动画 | CSS 动画与过渡 | animationstart、transitionend |
| 网络 | 在线状态、网络请求 | online、offline、fetch |
| 存储 | 本地存储变化监听 | storage、indexedDB 事务 |
| 设备与传感器 | 设备方向、加速度、环境光线、屏幕方向 | orientationchange、deviceorientation、devicemotion |
| 触摸与手势 | 多点触控、手势缩放/旋转 | touchstart、touchmove、gesturechange |
| 全屏 | 全屏模式切换 | fullscreenchange、fullscreenerror |
| 挂起与可见性 | 页面可见性、页面生命周期 | visibilitychange、pagehide、pageshow |
DOM3 Events在DOM2 Events基础上重新定义了事件系统,主流浏览器均已支持。HTML Living Standard、Pointer Events 等规范在此基础上不断扩展。
DOM 事件模型概览
事件传播机制
捕获阶段(Capture Phase) → 目标阶段(Target Phase) → 冒泡阶段(Bubble Phase)
document ↓ ↓ ↑ document
↓ target ↑
body ↓ ↓ ↑ body
↓ 元素 ↑
元素 ←←←←←←←←←←←←←←←←←←←←←←←←←←←←- 捕获阶段:事件从
document向下传播到目标元素 - 目标阶段:事件到达目标元素
- 冒泡阶段:事件从目标元素向上冒泡到
document(默认监听阶段)
// 在捕获阶段触发
element.addEventListener("click", handler, { capture: true })
// 或简写
element.addEventListener("click", handler, true)
// 在冒泡阶段触发(默认)
element.addEventListener("click", handler, { capture: false })事件对象常用属性与方法
| 属性/方法 | 说明 |
|---|---|
event.type | 事件类型 |
event.target | 触发事件的元素(实际点击的元素) |
event.currentTarget | 当前处理事件的元素(绑定监听器的元素) |
event.bubbles | 事件是否冒泡 |
event.cancelable | 事件是否可取消默认行为 |
event.defaultPrevented | 是否已调用 preventDefault() |
event.timeStamp | 事件触发的时间戳 |
event.isTrusted | 是否由用户操作触发(true)或脚本触发(false) |
preventDefault() | 取消事件的默认行为 |
stopPropagation() | 阻止事件继续传播(捕获和冒泡) |
stopImmediatePropagation() | 阻止事件传播并阻止同一元素上的其他监听器 |
事件委托
事件委托利用事件冒泡机制,在父元素上监听子元素的事件,减少监听器数量并支持动态元素。
// ❌ 不推荐:为每个元素单独绑定
document.querySelectorAll(".item").forEach((item) => {
item.addEventListener("click", handleClick)
})
// ✅ 推荐:事件委托
document.querySelector(".list").addEventListener("click", (event) => {
// 检查点击的是否为目标元素
if (event.target.matches(".item")) {
console.log("点击的项目:", event.target.textContent)
// event.currentTarget 是 .list
// event.target 是被点击的 .item
}
})
// 处理动态添加的元素
const list = document.querySelector(".list")
list.addEventListener("click", (event) => {
const item = event.target.closest(".item")
if (item && list.contains(item)) {
console.log("项目 ID:", item.dataset.id)
}
})监听器选项
// 完整选项对象
element.addEventListener("click", handler, {
capture: false, // 是否在捕获阶段触发
once: true, // 是否只触发一次后自动移除
passive: true, // 是否不会调用 preventDefault()
signal: abortController.signal // AbortSignal,用于取消监听
})
// once 选项 - 自动移除
element.addEventListener("click", () => {
console.log("只触发一次")
}, { once: true })
// passive 选项 - 提升滚动性能
element.addEventListener("touchstart", handler, { passive: true })
element.addEventListener("wheel", handler, { passive: true })
// 使用 AbortController 统一取消多个监听器
const controller = new AbortController()
element1.addEventListener("click", handler1, { signal: controller.signal })
element2.addEventListener("click", handler2, { signal: controller.signal })
// 一次性取消所有监听器
controller.abort()自定义事件
// 创建自定义事件
const customEvent = new CustomEvent("userLogin", {
bubbles: true,
cancelable: true,
detail: {
userId: 123,
username: "john"
}
})
// 监听自定义事件
document.addEventListener("userLogin", (event) => {
console.log("用户登录:", event.detail.username)
})
// 触发自定义事件
document.dispatchEvent(customEvent)用户界面事件
用户界面事件(UI Events)不一定与直接的用户操作有关,它们更多是对文档/资源状态或窗口变化的通知。
UI 事件速查表
| 事件 | 触发节点 | 说明与现代建议 |
|---|---|---|
load | window、资源元素 | 资源全部加载后触发,SPA 更常使用 DOMContentLoaded 或资源级 load |
DOMContentLoaded | document | DOM 解析完成后触发,不等待样式表、图片等资源 |
unload | window | 页面卸载后触发,现代浏览器多限制其行为,建议转向 pagehide/visibilitychange |
beforeunload | window | 页面卸载前触发,可提示用户确认 |
abort | <object> 等 | 资源加载被用户取消时触发,现代场景较少用 |
error | window、资源元素 | 捕获加载失败与脚本错误;脚本可通过 addEventListener("error", handler, true) 捕获 |
select | 可输入控件 | 文本被选中时触发,常用于同步输入与辅助提示 |
resize | window | 窗口尺寸变更;对于元素尺寸变化,使用 ResizeObserver |
scroll | window、可滚动容器 | 滚动位置变化,高频事件需配合节流、防抖或 requestAnimationFrame |
load 事件
load 事件在所有资源(包括图片、样式表、脚本等)加载完成后触发。
// 监听页面完全加载
window.addEventListener("load", (event) => {
console.log("页面及所有资源加载完成")
})
// 监听单个图片加载
const image = document.getElementById("myImage")
image.addEventListener("load", (event) => {
console.log("图片加载完成:", event.target.src)
})
// 动态创建图片并监听加载
const newImage = new Image()
newImage.addEventListener("load", (event) => {
console.log("图片加载完成")
})
// 设置 src 触发加载(无需添加到 DOM)
newImage.src = "image.jpg"
// 监听脚本加载
const script = document.createElement("script")
script.addEventListener("load", () => {
console.log("脚本加载完成")
})
script.src = "library.js"
document.body.appendChild(script)ℹ️ 最佳实践 若目标是尽快执行脚本或初始化交互,优先监听
DOMContentLoaded或将脚本置于<body>末尾。
DOMContentLoaded 事件
DOMContentLoaded 在 DOM 解析完成后立即触发,不等待样式表、图片等外部资源。
document.addEventListener("DOMContentLoaded", (event) => {
console.log("DOM 已就绪,可以安全操作 DOM")
// 此时可以访问和操作 DOM 元素
const app = document.getElementById("app")
app.innerHTML = "<p>应用已初始化</p>"
})执行顺序对比:
document.addEventListener("DOMContentLoaded", () => {
console.log("1. DOMContentLoaded - DOM 就绪")
})
window.addEventListener("load", () => {
console.log("2. load - 所有资源加载完成")
})
// 控制台输出:
// 1. DOMContentLoaded - DOM 就绪
// 2. load - 所有资源加载完成unload 与 pagehide 事件
⚠️ 重要提示 自 2020 年起,Chrome、Safari 等在部分场景不再保证
unload触发,推荐使用pagehide、visibilitychange或持久化机制(如navigator.sendBeacon)。
// ❌ 不推荐:unload 事件不可靠
window.addEventListener("unload", () => {
// 这里的代码可能不会执行
localStorage.setItem("data", JSON.stringify(data))
})
// ✅ 推荐:使用 pagehide 事件
window.addEventListener("pagehide", (event) => {
// event.persisted 表示页面是否被缓存(bfcache)
if (event.persisted) {
console.log("页面被缓存到 bfcache")
}
})
// ✅ 推荐:使用 sendBeacon 发送数据
window.addEventListener("pagehide", () => {
navigator.sendBeacon("/analytics", JSON.stringify({
action: "page_close",
duration: performance.now()
}))
})
// ✅ 推荐:使用 visibilitychange 处理数据保存
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
saveDataToServer()
}
})beforeunload 事件
用于在用户离开页面前提示确认。
let hasUnsavedChanges = false
// 标记有未保存的更改
function markUnsaved() {
hasUnsavedChanges = true
}
window.addEventListener("beforeunload", (event) => {
if (hasUnsavedChanges) {
// 现代浏览器会忽略自定义消息,只显示默认提示
event.preventDefault()
event.returnValue = "" // Chrome 需要这个
return "" // 旧版浏览器需要这个
}
})
// 用户保存后清除标记
function saveChanges() {
// ... 保存逻辑
hasUnsavedChanges = false
}⚠️ 注意事项
- 现代浏览器出于防骚扰考虑,只在用户交互后触发对话框
- 自定义文案会被忽略,只显示浏览器默认提示
- 不要滥用此事件,影响用户体验
resize 事件
// 监听窗口尺寸变化
window.addEventListener("resize", (event) => {
console.log("窗口尺寸:", window.innerWidth, "x", window.innerHeight)
})
// ❌ 问题:resize 触发非常频繁
window.addEventListener("resize", () => {
// 每次像素变化都会触发,性能问题
updateLayout()
})
// ✅ 推荐:使用节流限制触发频率
let resizeTimer
window.addEventListener("resize", () => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(updateLayout, 200)
})
// ✅ 更优:用 ResizeObserver 只监听目标元素尺寸变化
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
console.log("元素尺寸:", width, "x", height)
}
})
resizeObserver.observe(element)scroll 事件
// 获取滚动位置(跨浏览器兼容)
function getScrollPosition() {
return {
x: window.pageXOffset || document.documentElement.scrollLeft,
y: window.pageYOffset || document.documentElement.scrollTop
}
}
// ❌ 问题:scroll 触发非常频繁
window.addEventListener("scroll", () => {
// 每次滚动都会触发,性能问题
checkVisibleElements()
})
// ✅ 更优:用 IntersectionObserver 监听元素可见性
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
checkVisibleElements()
}
})
}, { threshold: 0.1 })
document.querySelectorAll(".lazy-load").forEach((el) => {
observer.observe(el)
})焦点事件
焦点事件在页面元素获得或失去焦点时触发,对于表单验证和无障碍访问非常重要。
焦点事件列表
| 事件 | 冒泡 | 说明 |
|---|---|---|
focus | 否 | 元素获得焦点时触发 |
blur | 否 | 元素失去焦点时触发 |
focusin | 是 | 元素获得焦点时触发(focus 的冒泡版本) |
focusout | 是 | 元素失去焦点时触发(blur 的冒泡版本) |
基本用法
const input = document.querySelector("input")
// focus/blur 不冒泡
input.addEventListener("focus", () => {
input.classList.add("focused")
})
input.addEventListener("blur", () => {
input.classList.remove("focused")
validateInput(input)
})
// focusin/focusout 冒泡,适合事件委托
document.querySelector("form").addEventListener("focusin", (event) => {
if (event.target.matches("input, textarea")) {
event.target.closest(".form-group").classList.add("focused")
}
})
document.querySelector("form").addEventListener("focusout", (event) => {
if (event.target.matches("input, textarea")) {
event.target.closest(".form-group").classList.remove("focused")
}
})获取焦点信息
// 当前获得焦点的元素
console.log("当前焦点元素:", document.activeElement)
// 文档是否获得焦点
console.log("文档是否有焦点:", document.hasFocus())
// 监听全局焦点变化
document.addEventListener("focusin", (event) => {
console.log("焦点移入:", event.target.tagName)
})
document.addEventListener("focusout", (event) => {
console.log("焦点移出:", event.target.tagName)
// 相关元素:即将获得焦点的元素
console.log("下一个焦点元素:", event.relatedTarget)
})焦点管理
// 编程方式设置焦点
const input = document.querySelector("input")
input.focus() // 获取焦点
input.blur() // 移除焦点
// 设置焦点并选中内容
input.focus()
input.select()
// 焦点陷阱(模态框)
function trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
element.addEventListener("keydown", (event) => {
if (event.key === "Tab") {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault()
lastElement.focus()
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault()
firstElement.focus()
}
}
})
}鼠标与滚轮事件
鼠标事件列表
| 事件 | 冒泡 | 说明 |
|---|---|---|
click | 是 | 单击主鼠标按钮或按回车键时触发 |
dblclick | 是 | 双击主鼠标按钮时触发 |
mousedown | 是 | 按下任意鼠标按钮时触发 |
mouseup | 是 | 释放鼠标按钮时触发 |
mousemove | 是 | 鼠标指针在元素内移动时持续触发 |
mouseenter | 否 | 鼠标首次进入元素范围时触发 |
mouseleave | 否 | 鼠标离开元素范围时触发 |
mouseover | 是 | 鼠标进入元素或其子元素时触发 |
mouseout | 是 | 鼠标离开元素或进入其子元素时触发 |
contextmenu | 是 | 右键点击时触发,用于自定义右键菜单 |
事件触发顺序
单击:
mousedown → mouseup → click双击:
mousedown → mouseup → click → mousedown → mouseup → click → dblclick鼠标移入移出:
mouseover → mouseenter → (mousemove...) → mouseout → mouseleave鼠标事件对象属性
element.addEventListener("click", (event) => {
// 坐标位置
console.log("视口坐标:", event.clientX, event.clientY)
console.log("页面坐标:", event.pageX, event.pageY)
console.log("屏幕坐标:", event.screenX, event.screenY)
// 相对于目标元素的偏移
console.log("元素内偏移:", event.offsetX, event.offsetY)
// 按钮信息
console.log("按钮:", event.button) // 0=左键, 1=中键, 2=右键
console.log("按钮组合:", event.buttons) // 位掩码
// 修改键状态
console.log("Ctrl:", event.ctrlKey)
console.log("Shift:", event.shiftKey)
console.log("Alt:", event.altKey)
console.log("Meta:", event.metaKey) // Mac 的 Command 键
// 其他信息
console.log("点击次数:", event.detail)
console.log("目标元素:", event.target)
console.log("当前元素:", event.currentTarget)
})坐标系统示意图
┌─────────────────────────────────────────────────────────────┐
│ Screen (screenX/Y) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Browser Window │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Viewport (clientX/Y) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ Element │ │ │ │
│ │ │ │ (offsetX/Y) │ │ │ │
│ │ │ │ •click │ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Document (pageX/Y)mouseenter/mouseleave vs mouseover/mouseout
const container = document.querySelector(".container")
const child = document.querySelector(".child")
// mouseenter/mouseleave:不冒泡,进入子元素不会触发
container.addEventListener("mouseenter", () => {
console.log("mouseenter: 鼠标进入容器")
// 鼠标从容器移到子元素时不会触发
})
container.addEventListener("mouseleave", () => {
console.log("mouseleave: 鼠标离开容器")
})
// mouseover/mouseout:冒泡,进入子元素会触发
container.addEventListener("mouseover", (event) => {
console.log("mouseover: 鼠标进入", event.target.tagName)
// 鼠标从容器移到子元素时会触发(事件冒泡)
})
container.addEventListener("mouseout", (event) => {
console.log("mouseout: 鼠标离开", event.target.tagName)
})滚轮事件
element.addEventListener("wheel", (event) => {
// 滚动方向和距离
console.log("水平滚动:", event.deltaX)
console.log("垂直滚动:", event.deltaY)
console.log("Z轴滚动:", event.deltaZ)
// 滚动单位
console.log("滚动模式:", event.deltaMode)
// 0 = 像素 (DOM_DELTA_PIXEL)
// 1 = 行 (DOM_DELTA_LINE)
// 2 = 页 (DOM_DELTA_PAGE)
// 阻止默认滚动行为
event.preventDefault()
}, { passive: false }) // 必须设置 passive: false 才能阻止默认行为自定义右键菜单
document.addEventListener("contextmenu", (event) => {
// 阻止默认右键菜单
event.preventDefault()
// 显示自定义菜单
const menu = document.getElementById("custom-menu")
menu.style.left = `${event.pageX}px`
menu.style.top = `${event.pageY}px`
menu.style.display = "block"
})
// 点击其他地方隐藏菜单
document.addEventListener("click", () => {
document.getElementById("custom-menu").style.display = "none"
})指针事件(Pointer Events)
Pointer Events 是 W3C 制定的标准,旨在统一处理鼠标、触摸、笔等多种输入设备的事件。相比传统的鼠标事件和触摸事件,Pointer Events 提供了更一致、更强大的接口。
指针事件列表
| 事件 | 冒泡 | 说明 |
|---|---|---|
pointerdown | 是 | 指针变为活动状态时触发 |
pointerup | 是 | 指针变为非活动状态时触发 |
pointermove | 是 | 指针移动时触发 |
pointerover | 是 | 指针进入元素时触发 |
pointerout | 是 | 指针离开元素时触发 |
pointerenter | 否 | 指针进入元素时触发(不冒泡) |
pointerleave | 否 | 指针离开元素时触发(不冒泡) |
pointercancel | 是 | 系统取消指针事件时触发 |
Pointer Events 的核心优势:
- 统一输入模型:一套 API 处理鼠标、触摸、笔等所有输入设备
- 支持多点触控:通过
pointerId区分不同的触控点 - 支持笔输入:包含压力、倾斜等特有属性
- 自动处理兼容性:浏览器会自动将触摸转换为鼠标事件
指针事件对象属性
element.addEventListener("pointerdown", (event) => {
// 基本信息
console.log("指针ID:", event.pointerId)
console.log("指针类型:", event.pointerType) // "mouse", "pen", "touch"
console.log("是否主指针:", event.isPrimary)
// 按钮信息
console.log("按钮:", event.button) // -1=无, 0=左/触控, 1=中, 2=右, 3=后退, 4=前进
console.log("按钮组合:", event.buttons)
// 坐标(同鼠标事件)
console.log("视口坐标:", event.clientX, event.clientY)
console.log("页面坐标:", event.pageX, event.pageY)
// 触摸和笔特有属性
console.log("压力:", event.pressure) // 0-1
console.log("宽度:", event.width) // 接触区域
console.log("高度:", event.height)
// 笔输入特有属性
console.log("倾斜X:", event.tiltX) // -90 到 90
console.log("倾斜Y:", event.tiltY) // -90 到 90
console.log("扭转角度:", event.twist) // 0-359
console.log("切向压力:", event.tangentialPressure)
})指针捕获
指针捕获允许特定元素捕获指针事件,即使指针移出元素边界,事件仍会派发到该元素:
const slider = document.querySelector(".slider")
slider.addEventListener("pointerdown", (event) => {
// 捕获指针
slider.setPointerCapture(event.pointerId)
// 开始拖动
slider.classList.add("dragging")
const onMove = (event) => {
// 计算新位置
const rect = slider.getBoundingClientRect()
const newValue = (event.clientX - rect.left) / rect.width * 100
slider.style.setProperty("--value", `${Math.max(0, Math.min(100, newValue))}%`)
}
const onUp = () => {
// 释放指针捕获
slider.releasePointerCapture(event.pointerId)
slider.classList.remove("dragging")
slider.removeEventListener("pointermove", onMove)
slider.removeEventListener("pointerup", onUp)
}
slider.addEventListener("pointermove", onMove)
slider.addEventListener("pointerup", onUp)
})指针事件与触摸事件对比
// ❌ 传统方式:分别监听鼠标和触摸
element.addEventListener("mousedown", handleMouse)
element.addEventListener("touchstart", handleTouch)
// ✅ 推荐:使用指针事件统一处理
element.addEventListener("pointerdown", handlePointer)
function handlePointer(event) {
// 根据指针类型执行不同逻辑
switch (event.pointerType) {
case "mouse":
console.log("鼠标输入")
break
case "touch":
console.log("触摸输入")
break
case "pen":
console.log("笔输入")
break
}
}多点触控处理
const activePointers = new Map()
element.addEventListener("pointerdown", (event) => {
activePointers.set(event.pointerId, {
x: event.clientX,
y: event.clientY
})
console.log(`活跃指针数: ${activePointers.size}`)
})
element.addEventListener("pointermove", (event) => {
if (activePointers.has(event.pointerId)) {
// 更新该指针的最新位置
activePointers.set(event.pointerId, {
x: event.clientX,
y: event.clientY
})
}
})
element.addEventListener("pointerup", (event) => {
activePointers.delete(event.pointerId)
})ℹ️ 浏览器支持 Pointer Events 在所有现代浏览器中得到良好支持。对于旧浏览器,可以使用 PEP (Pointer Events Polyfill) 进行兼容。
拖放事件(Drag & Drop Events)
HTML5 原生拖放 API 允许实现元素的拖拽功能,支持文件拖放、元素拖放等场景。
拖放事件列表
| 事件 | 触发时机 | 目标元素 |
|---|---|---|
dragstart | 开始拖动时 | 被拖动元素 |
drag | 拖动过程中持续触发 | 被拖动元素 |
dragend | 拖动结束时 | 被拖动元素 |
dragenter | 拖动进入目标元素时 | 目标元素 |
dragover | 在目标元素上方拖动时 | 目标元素 |
dragleave | 拖动离开目标元素时 | 目标元素 |
drop | 在目标元素上放下时 | 目标元素 |
基本拖放示例
<style>
.draggable {
width: 100px;
height: 100px;
background: #3498db;
cursor: move;
user-select: none;
}
.dropzone {
width: 200px;
height: 200px;
border: 2px dashed #ccc;
}
</style>
<div id="drag-item" class="draggable" draggable="true">拖动我</div>
<div id="dropzone" class="dropzone">放到这里</div>
<script>
const dragItem = document.getElementById("drag-item")
const dropzone = document.getElementById("dropzone")
dragItem.addEventListener("dragstart", (event) => {
event.dataTransfer.setData("text/plain", dragItem.id)
event.dataTransfer.effectAllowed = "move"
})
dropzone.addEventListener("dragover", (event) => {
event.preventDefault()
event.dataTransfer.dropEffect = "move"
})
dropzone.addEventListener("drop", (event) => {
event.preventDefault()
// 获取拖动数据
const id = event.dataTransfer.getData("text/plain")
const draggedElement = document.getElementById(id)
dropzone.appendChild(draggedElement)
})
</script>DataTransfer 对象
// 设置数据
event.dataTransfer.setData("text/plain", "文本数据")
event.dataTransfer.setData("text/uri-list", "https://example.com")
event.dataTransfer.setData("application/json", JSON.stringify({ id: 1 }))
// 获取数据
const text = event.dataTransfer.getData("text/plain")
const json = JSON.parse(event.dataTransfer.getData("application/json"))
// 设置拖动效果
event.dataTransfer.effectAllowed = "move" // "none", "copy", "copyLink", "copyMove", "link", "linkMove", "move", "all"
event.dataTransfer.dropEffect = "move" // "none", "copy", "link", "move"
// 设置拖动图像
const img = new Image()
img.src = "drag-image.png"
event.dataTransfer.setDragImage(img, 10, 10) // 图像和偏移量
// 获取文件列表
const files = event.dataTransfer.files文件拖放
const dropzone = document.getElementById("dropzone")
dropzone.addEventListener("dragover", (event) => {
event.preventDefault()
})
dropzone.addEventListener("drop", (event) => {
event.preventDefault()
// 获取拖放的文件
const files = event.dataTransfer.files
Array.from(files).forEach((file) => {
const reader = new FileReader()
// 按文件类型选择读取方式
if (file.type.startsWith("image/")) {
reader.readAsDataURL(file) // 图片读取为 DataURL
reader.onload = () => {
console.log("图片预览:", reader.result)
}
} else {
reader.readAsText(file) // 文本读取为字符串
reader.onload = () => {
console.log("文件内容:", reader.result)
}
}
})
})拖放排序示例
const list = document.querySelector(".sortable-list")
let draggedItem = null
list.addEventListener("dragstart", (event) => {
if (event.target.matches("li")) {
draggedItem = event.target
event.target.style.opacity = "0.5"
}
})
list.addEventListener("dragover", (event) => {
event.preventDefault()
})
list.addEventListener("drop", (event) => {
event.preventDefault()
if (!draggedItem) return
const afterElement = getDragAfterElement(list, event.clientY)
if (afterElement == null) {
list.appendChild(draggedItem)
} else {
list.insertBefore(draggedItem, afterElement)
}
})
list.addEventListener("dragend", (event) => {
if (event.target.matches("li")) {
event.target.style.opacity = ""
draggedItem = null
}
})
// 根据鼠标位置确定插入的目标节点
function getDragAfterElement(container, y) {
const draggable = [...container.querySelectorAll("li:not(.dragging)")]
return draggable.reduce((closest, child) => {
const box = child.getBoundingClientRect()
const offset = y - box.top - box.height / 2
if (offset < 0 && offset > closest.offset) {
return { offset, element: child }
} else {
return closest
}
}, { offset: Number.NEGATIVE_INFINITY }).element
}⚠️ 注意 在
dragover事件中必须调用event.preventDefault(),否则drop事件不会触发。
键盘事件
键盘事件列表
| 事件 | 说明 |
|---|---|
keydown | 按下任意键时触发,按住不放会重复触发 |
keyup | 释放键盘上的键时触发 |
键盘事件对象属性
document.addEventListener("keydown", (event) => {
// 按键信息(推荐)
console.log("按键值:", event.key) // "a", "Enter", "ArrowUp", "Escape"
console.log("物理键位:", event.code) // "KeyA", "Enter", "ArrowUp", "Escape"
// 已废弃但广泛支持
console.log("键码(已废弃):", event.keyCode)
console.log("字符码(已废弃):", event.charCode)
// 修饰键状态
console.log("Ctrl:", event.ctrlKey)
console.log("Shift:", event.shiftKey)
console.log("Alt:", event.altKey)
console.log("Meta:", event.metaKey)
// 其他信息
console.log("重复按键:", event.repeat)
console.log("输入区域:", event.location) // 0=标准, 1=左, 2=右, 3=数字键盘
})key 与 code 的区别
document.addEventListener("keydown", (event) => {
// event.key: 按键的语义值(受键盘布局和修饰键影响)
// event.code: 物理键位(不受布局影响)
// 示例:在 QWERTY 键盘上按 "a" 键
// key: "a" (小写) 或 "A" (按住 Shift)
// code: "KeyA" (始终不变)
// 示例:在 Dvorak 键盘上按同一位置的键
// key: ";" (Dvorak 布局)
// code: "KeyA" (物理位置不变)
})常用按键值
| key 值 | code 值 | 说明 |
|---|---|---|
"a" - "z" | "KeyA" - "KeyZ" | 字母键 |
"0" - "9" | "Digit0" - "Digit9" | 数字键 |
"F1" - "F12" | "F1" - "F12" | 功能键 |
"Enter" | "Enter" | 回车键 |
"Escape" | "Escape" | Esc 键 |
"Space" | "Space" | 空格键 |
"Tab" | "Tab" | Tab 键 |
"Backspace" | "Backspace" | 退格键 |
"Delete" | "Delete" | 删除键 |
"ArrowUp" | "ArrowUp" | 方向键上 |
"ArrowDown" | "ArrowDown" | 方向键下 |
"ArrowLeft" | "ArrowLeft" | 方向键左 |
"ArrowRight" | "ArrowRight" | 方向键右 |
"Home" | "Home" | Home 键 |
"End" | "End" | End 键 |
"PageUp" | "PageUp" | PageUp 键 |
"PageDown" | "PageDown" | PageDown 键 |
快捷键实现
// 全局快捷键
document.addEventListener("keydown", (event) => {
// Ctrl/Cmd + S 保存
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault()
saveDocument()
}
// Ctrl/Cmd + Shift + S 另存为
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "s") {
event.preventDefault()
saveAs()
}
// 游戏方向键控制
if (event.key === "ArrowDown") player.move("down")
if (event.key === "ArrowLeft") player.move("left")
if (event.key === "ArrowRight") player.move("right")
requestAnimationFrame(gameLoop)
})无障碍访问考虑
// ✅ 正确:同时支持键盘和鼠标操作
button.addEventListener("click", handleAction)
button.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
handleAction()
}
})
// ✅ 正确:提供键盘导航
menu.addEventListener("keydown", (event) => {
const items = Array.from(menu.querySelectorAll(".menu-item"))
const index = items.indexOf(document.activeElement)
switch (event.key) {
case "ArrowDown":
event.preventDefault()
items[(index + 1) % items.length].focus()
break
case "ArrowUp":
event.preventDefault()
items[(index - 1 + items.length) % items.length].focus()
break
case "Escape":
menu.close()
break
}
})输入与文本事件
input 事件
input 事件在 <input>、<textarea> 或 contenteditable 元素值发生变化时触发。
const input = document.querySelector('input[type="text"]')
input.addEventListener("input", (event) => {
console.log("输入值:", event.target.value)
console.log("事件类型:", event.inputType)
console.log("插入的数据:", event.data)
})inputType 值列表
| inputType | 说明 |
|---|---|
insertText | 插入文本 |
insertFromPaste | 粘贴 |
insertFromDrop | 拖放插入 |
deleteContentBackward | 向后删除(Backspace) |
deleteContentForward | 向前删除(Delete) |
deleteByCut | 剪切删除 |
deleteByDrag | 拖动删除 |
insertLineBreak | 插入换行 |
insertParagraph | 插入段落 |
formatBold | 加粗(contenteditable) |
formatItalic | 斜体(contenteditable) |
historyUndo | 撤销 |
historyRedo | 重做 |
beforeinput 事件
beforeinput 事件在输入实际发生之前触发,可以取消输入操作:
const input = document.querySelector("input")
input.addEventListener("beforeinput", (event) => {
// 限制只能输入数字
if (event.inputType === "insertText" && !/^\d$/.test(event.data)) {
event.preventDefault()
console.log("只允许输入数字")
}
})
// 邮箱格式限制
emailInput.addEventListener("beforeinput", (event) => {
if (event.inputType === "insertText") {
const newValue = event.target.value.slice(0, event.target.selectionStart) +
event.data +
event.target.value.slice(event.target.selectionEnd)
if (!/^[a-zA-Z0-9@._-]*$/.test(newValue)) {
event.preventDefault()
}
}
})复合事件(IME 输入)
复合事件用于处理 IME(输入法编辑器)的输入序列,如中文输入。
const input = document.querySelector("input")
input.addEventListener("compositionstart", (event) => {
console.log("开始输入法编辑")
})
input.addEventListener("compositionupdate", (event) => {
console.log("正在编辑:", event.data)
})
input.addEventListener("compositionend", (event) => {
console.log("输入法编辑完成:", event.data)
isComposing = false
})
input.addEventListener("input", () => {
// 仅在非输入法状态下实时搜索
if (!isComposing) {
performSearch(input.value)
}
})实时搜索示例
<input type="text" id="searchInput" placeholder="搜索..." />
<ul id="searchResults"></ul>
<script>
const searchInput = document.getElementById("searchInput")
const searchResults = document.getElementById("searchResults")
let debounceTimer
let isComposing = false
// 处理输入法
searchInput.addEventListener("compositionstart", () => {
isComposing = true
})
searchInput.addEventListener("compositionend", () => {
isComposing = false
searchInput.dispatchEvent(new Event("input"))
})
// 防抖搜索
searchInput.addEventListener("input", () => {
if (isComposing) return
clearTimeout(debounceTimer)
debounceTimer = setTimeout(async () => {
const results = await fetch(`/api/search?q=${searchInput.value}`).then(r => r.json())
renderResults(results)
}, 300)
})
function renderResults(results) {
searchResults.innerHTML = results
.map(item => `<li>${item.name}</li>`)
.join("")
}
</script>表单事件
submit 和 reset 事件
<form id="myForm">
<input type="text" name="username" required />
<input type="email" name="email" required />
<button type="submit">提交</button>
<button type="reset">重置</button>
</form>
<script>
const form = document.getElementById("myForm")
form.addEventListener("submit", (event) => {
event.preventDefault() // 阻止默认提交
// 收集表单数据
const formData = new FormData(form)
const data = Object.fromEntries(formData.entries())
console.log("提交数据:", data)
// 发送请求
fetch("/api/submit", {
method: "POST",
body: JSON.stringify(data)
})
})
// reset 事件:重置前确认
form.addEventListener("reset", (event) => {
const confirmed = confirm("确定要重置表单吗?")
if (!confirmed) {
event.preventDefault()
}
})
</script>change 事件
change 事件在表单控件的值发生变化并失去焦点时触发。
const select = document.querySelector("select")
const checkbox = document.querySelector('input[type="checkbox"]')
const radio = document.querySelector('input[type="radio"]')
select.addEventListener("change", (event) => {
console.log("选中选项:", event.target.value)
})
checkbox.addEventListener("change", (event) => {
console.log("勾选状态:", event.target.checked)
})
// 单选按钮组
document.querySelectorAll('input[name="gender"]').forEach((radio) => {
radio.addEventListener("change", (event) => {
console.log("选择性别:", event.target.value)
})
})input 与 change 的区别
const textInput = document.querySelector('input[type="text"]')
// input:每次输入都触发(实时)
textInput.addEventListener("input", (event) => {
console.log("input事件:", event.target.value)
})
// change:失去焦点且值改变时触发
textInput.addEventListener("change", (event) => {
console.log("change事件:", event.target.value)
})ℹ️ 区别说明
input事件适合实时响应,如搜索框、字数统计change事件适合最终确认,如提交前的验证- 对于
<select>、<input type="checkbox">等控件,change事件会立即触发
表单验证
const form = document.querySelector("form")
// 提交时验证
form.addEventListener("submit", (event) => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add("was-validated")
})
// 确认密码实时校验
const password = document.querySelector('input[name="password"]')
const confirmPassword = document.querySelector('input[name="confirmPassword"]')
confirmPassword.addEventListener("input", () => {
if (confirmPassword.value !== password.value) {
confirmPassword.setCustomValidity("两次密码输入不一致")
} else {
confirmPassword.setCustomValidity("")
}
})剪贴板事件
基本剪贴板事件
| 事件 | 触发时机 |
|---|---|
copy | 复制操作时触发 |
cut | 剪切操作时触发 |
paste | 粘贴操作时触发 |
监听剪贴板事件
document.addEventListener("copy", (event) => {
// 获取选中的文本
const selection = window.getSelection().toString()
// 自定义复制内容
event.clipboardData.setData("text/plain", `复制自文档:${selection}`)
event.clipboardData.setData("text/html", `<strong>${selection}</strong>`)
// 阻止默认复制行为
event.preventDefault()
console.log("已复制:", selection)
})
document.addEventListener("paste", (event) => {
// 获取剪贴板数据
const clipboardData = event.clipboardData || window.clipboardData
// 获取不同格式的数据
const text = clipboardData.getData("text/plain")
const html = clipboardData.getData("text/html")
console.log("粘贴的文本:", text)
console.log("粘贴的HTML:", html)
})异步剪贴板 API
// 写入剪贴板
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text)
console.log("复制成功")
} catch (err) {
console.error("复制失败:", err)
}
}
// 读取剪贴板(含图片)
async function readFromClipboard() {
try {
const items = await navigator.clipboard.read()
for (const item of items) {
for (const type of item.types) {
if (type.startsWith("image/")) {
const blob = await item.getType(type)
const url = URL.createObjectURL(blob)
const img = document.createElement("img")
img.src = url
document.body.appendChild(img)
} else if (type === "text/plain") {
const text = await item.getType(type).then(b => b.text())
console.log("剪贴板文本:", text)
}
}
}
} catch (err) {
console.error("读取剪贴板失败:", err)
}
}⚠️ 权限和兼容性
- 异步剪贴板 API 需要 HTTPS 环境
- 读取剪贴板需要用户授权或由用户操作触发
navigator.clipboard在部分浏览器中可能不完全支持,需要特性检测
// 特性检测与降级
async function safeCopy(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
// 使用现代 API
await navigator.clipboard.writeText(text)
} else {
// 降级到传统方式
const textarea = document.createElement("textarea")
textarea.value = text
textarea.style.position = "fixed"
textarea.style.opacity = "0"
document.body.appendChild(textarea)
textarea.select()
document.execCommand("copy")
document.body.removeChild(textarea)
}
}多媒体事件
多媒体事件用于处理音视频元素的状态变化和用户交互。
媒体事件列表
| 事件 | 说明 |
|---|---|
play | 开始播放时触发 |
pause | 暂停播放时触发 |
ended | 播放结束时触发 |
timeupdate | 播放位置更新时触发 |
loadedmetadata | 元数据加载完成时触发 |
waiting | 因缓冲而等待时触发 |
ratechange | 播放速度变化时触发 |
progress | 下载进度变化时触发 |
error | 发生错误时触发 |
基本使用
const video = document.querySelector("video")
// 监听播放状态
video.addEventListener("play", () => {
console.log("开始播放")
})
video.addEventListener("pause", () => {
console.log("暂停播放")
})
video.addEventListener("ended", () => {
console.log("播放结束")
})
// 监听播放进度
video.addEventListener("timeupdate", () => {
const progress = (video.currentTime / video.duration) * 100
console.log(`播放进度: ${progress.toFixed(1)}%`)
})
// 监听加载状态
video.addEventListener("loadedmetadata", () => {
console.log("视频时长:", video.duration)
console.log("视频尺寸:", video.videoWidth, "x", video.videoHeight)
})
video.addEventListener("canplay", () => {
console.log("可以开始播放")
})
// 监听音量变化
video.addEventListener("volumechange", () => {
console.log("音量:", video.volume)
console.log("静音:", video.muted)
})自定义视频播放器
const video = document.querySelector("video")
const playBtn = document.getElementById("playBtn")
const progressBar = document.getElementById("progressBar")
const volumeSlider = document.getElementById("volumeSlider")
const currentTimeDisplay = document.getElementById("currentTime")
// 播放/暂停
playBtn.addEventListener("click", () => {
if (video.paused) {
video.play()
playBtn.textContent = "暂停"
} else {
video.pause()
playBtn.textContent = "播放"
}
})
// 更新进度条和时间
video.addEventListener("timeupdate", () => {
if (video.duration) {
const percent = (video.currentTime / video.duration) * 100
progressBar.value = percent
currentTimeDisplay.textContent = formatTime(video.currentTime)
}
})
function formatTime(seconds) {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, "0")}`
}动画事件
CSS 动画事件
| 事件 | 说明 |
|---|---|
animationstart | CSS 动画开始时触发 |
animationend | CSS 动画结束时触发 |
animationiteration | CSS 动画每次迭代时触发 |
const element = document.querySelector(".animated")
element.addEventListener("animationstart", (event) => {
console.log("动画开始:", event.animationName)
})
element.addEventListener("animationiteration", (event) => {
console.log("动画迭代:", event.animationName, "次数:", event.elapsedTime)
})
element.addEventListener("animationend", (event) => {
console.log("动画结束:", event.animationName)
// 动画结束后执行清理或其他操作
element.classList.remove("animated")
})
// 触发动画
element.classList.add("animated")
// 动态控制动画
const animations = element.getAnimations()
animations.forEach((animation) => {
animation.pause() // 暂停
animation.play() // 播放
animation.reverse() // 反向
animation.finish() // 完成
animation.cancel() // 取消
})CSS 过渡事件
| 事件 | 说明 |
|---|---|
transitionstart | CSS 过渡开始时触发 |
transitionend | CSS 过渡结束时触发 |
transitionrun | CSS 过渡创建时触发 |
transitioncancel | CSS 过渡取消时触发 |
const element = document.querySelector(".transition")
element.addEventListener("transitionstart", (event) => {
console.log("过渡开始:", event.propertyName)
})
element.addEventListener("transitionend", (event) => {
console.log("过渡结束:", event.propertyName)
console.log("持续时间:", event.elapsedTime, "秒")
})
element.addEventListener("transitioncancel", (event) => {
console.log("过渡取消:", event.propertyName)
})
// 触发过渡
element.classList.add("active")Web Animations API
// 使用 Web Animations API 创建动画
const element = document.querySelector(".box")
const animation = element.animate(
[
{ transform: "translateX(0)", opacity: 1 },
{ transform: "translateX(100px)", opacity: 0.5 },
{ transform: "translateX(200px)", opacity: 0 }
],
{
duration: 1000,
easing: "ease-in-out",
fill: "forwards" // 结束后保持最终状态
}
)
// 监听动画完成
animation.onfinish = () => {
console.log("动画完成")
}
animation.oncancel = () => {
console.log("动画取消")
}网络事件
online/offline 事件
// 监听网络状态变化
window.addEventListener("online", () => {
console.log("网络已连接")
showNotification("网络已恢复")
syncData()
})
window.addEventListener("offline", () => {
console.log("网络已断开")
showNotification("网络已断开,部分功能可能不可用")
})
// 检查当前网络状态
if (navigator.onLine) {
console.log("当前在线")
} else {
console.log("当前离线")
}Network Information API
// 获取网络信息
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection
if (connection) {
console.log("网络类型:", connection.effectiveType) // "4g", "3g", "2g", "slow-2g"
console.log("下行速度:", connection.downlink, "Mbps")
console.log("往返时间:", connection.rtt, "ms")
console.log("是否按流量计费:", connection.saveData)
// 监听网络变化
connection.addEventListener("change", () => {
console.log("网络状态变化:", connection.effectiveType)
// 根据网络质量调整内容
if (connection.effectiveType === "2g" || connection.saveData) {
loadLowQualityImages()
} else {
loadHighQualityImages()
}
})
}存储事件
storage 事件
当 localStorage 或 sessionStorage 被修改时,在其他同源页面触发:
// 监听存储变化(在其他标签页/窗口触发)
window.addEventListener("storage", (event) => {
console.log("键名:", event.key)
console.log("旧值:", event.oldValue)
console.log("新值:", event.newValue)
console.log "URL:", event.url
console.log("存储区域:", event.storageArea === localStorage ? "localStorage" : "sessionStorage")
// 同步数据到当前页面
if (event.key === "theme") {
applyTheme(event.newValue)
}
})
// 在当前页面修改存储
localStorage.setItem("theme", "dark") // 不会在当前页面触发 storage 事件
// 清除存储
localStorage.removeItem("theme")
localStorage.clear()IndexedDB 事件
// 打开数据库
const request = indexedDB.open("myDatabase", 1)
// 成功事件
request.addEventListener("success", (event) => {
const db = event.target.result
console.log("数据库打开成功")
})
// 错误事件
request.addEventListener("error", (event) => {
console.error("数据库打开失败:", event.target.error)
})
// 升级事件(创建/修改数据库结构)
request.addEventListener("upgradeneeded", (event) => {
const db = event.target.result
// 创建对象存储
if (!db.objectStoreNames.contains("users")) {
const store = db.createObjectStore("users", { keyPath: "id" })
store.createIndex("email", "email", { unique: true })
}
})全屏事件
const element = document.getElementById("fullscreenElement")
// 进入全屏
function enterFullscreen() {
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen()
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen()
}
}
// 监听全屏状态变化
document.addEventListener("fullscreenchange", () => {
console.log("全屏状态:", document.fullscreenElement ? "进入全屏" : "退出全屏")
updateFullscreenButton()
})
// 监听全屏错误
document.addEventListener("fullscreenerror", (event) => {
console.error("全屏错误:", event)
})
// 检查全屏状态
console.log("是否全屏:", !!document.fullscreenElement)
console.log("全屏元素:", document.fullscreenElement)可见性与生命周期事件
visibilitychange 事件
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
console.log("页面可见")
resumeVideo()
startPolling()
} else {
console.log("页面隐藏")
pauseVideo()
stopPolling()
// 保存数据
saveUserProgress()
}
})
// 检查可见状态
console.log("可见状态:", document.visibilityState) // "visible" 或 "hidden"
console.log("是否隐藏:", document.hidden)pageshow 和 pagehide 事件
// 页面显示(包括从 bfcache 恢复)
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
console.log("从 bfcache 恢复")
refreshData()
} else {
console.log("首次加载")
}
})
// 页面隐藏
window.addEventListener("pagehide", (event) => {
if (event.persisted) {
console.log("页面被缓存到 bfcache")
// 清理状态,避免恢复时出现问题
}
})设备与传感器事件
设备方向事件
// 监听设备方向
window.addEventListener("deviceorientation", (event) => {
// alpha: Z轴旋转角度(罗盘方向)0-360
// beta: X轴旋转角度(前后倾斜)-180 to 180
// gamma: Y轴旋转角度(左右倾斜)-90 to 90
console.log("Alpha:", event.alpha)
console.log("Beta:", event.beta)
console.log("Gamma:", event.gamma)
// 应用到元素旋转
const compass = document.getElementById("compass")
compass.style.transform = `rotate(${event.alpha}deg)`
})设备运动事件
// 监听设备运动
window.addEventListener("devicemotion", (event) => {
// 加速度(不含重力)
console.log("加速度:", event.acceleration)
// 加速度(含重力)
console.log("加速度含重力:", event.accelerationIncludingGravity)
// 旋转速率
console.log("旋转速率:", event.rotationRate)
// 检测摇晃
if (event.acceleration) {
const { x, y, z } = event.acceleration
const magnitude = Math.sqrt(x * x + y * y + z * z)
if (magnitude > 25) {
console.log("检测到摇晃")
handleShake()
}
}
})屏幕方向事件
// 监听屏幕方向变化
window.addEventListener("orientationchange", () => {
console.log("屏幕方向:", window.orientation)
// 0: 竖屏
// 90: 横屏(左)
// -90: 横屏(右)
// 180: 倒置竖屏
})
// 使用 Screen Orientation API
screen.orientation.addEventListener("change", () => {
console.log("方向类型:", screen.orientation.type) // "portrait-primary", "landscape-primary" 等
console.log("角度:", screen.orientation.angle) // 0, 90, 180, 270
})
// 锁定屏幕方向(需要全屏)
document.documentElement.requestFullscreen().then(() => {
screen.orientation.lock("landscape").then(() => {
console.log("锁定横屏")
})
})
// 解锁
screen.orientation.unlock()触摸与手势事件
触摸事件列表
| 事件 | 说明 |
|---|---|
touchstart | 手指触摸屏幕时触发 |
touchmove | 手指在屏幕上滑动时持续触发 |
touchend | 手指从屏幕上移开时触发 |
touchcancel | 系统停止跟踪触摸时触发 |
触摸事件对象
element.addEventListener("touchstart", (event) => {
// 阻止默认行为(如滚动、缩放)
event.preventDefault()
// 触摸点集合
console.log("所有触摸点:", event.touches)
console.log "当前元素的触摸点:", event.targetTouches
console.log("发生变化的触摸点:", event.changedTouches)
// 单个触摸点信息
const touch = event.touches[0]
console.log("触摸ID:", touch.identifier)
console.log("视口坐标:", touch.clientX, touch.clientY)
console.log("页面坐标:", touch.pageX, touch.pageY)
console.log("屏幕坐标:", touch.screenX, touch.screenY)
console.log("目标元素:", touch.target)
})多点触控示例
let touches = {}
element.addEventListener("touchstart", (event) => {
event.preventDefault()
for (const touch of event.changedTouches) {
touches[touch.identifier] = {
startX: touch.clientX,
startY: touch.clientY
}
}
})
element.addEventListener("touchmove", (event) => {
event.preventDefault()
// 双指缩放:计算两指距离变化
if (event.touches.length === 2) {
const distance = getDistance(event.touches[0], event.touches[1])
console.log("缩放距离:", distance)
}
})
element.addEventListener("touchend", (event) => {
for (const touch of event.changedTouches) {
delete touches[touch.identifier]
}
})
function getDistance(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX
const dy = touch1.clientY - touch2.clientY
return Math.sqrt(dx * dx + dy * dy)
}手势事件(iOS)
// 手势事件(仅 iOS Safari 支持)
element.addEventListener("gesturestart", (event) => {
console.log("手势开始")
})
element.addEventListener("gesturechange", (event) => {
console.log("旋转角度:", event.rotation)
console.log("缩放比例:", event.scale)
})
element.addEventListener("gestureend", (event) => {
console.log("手势结束")
})触摸与鼠标事件顺序
touchstart
touchend
mouseover
mousemove (一次)
mousedown
mouseup
click// 避免触摸设备上触发鼠标事件
let isTouchDevice = false
element.addEventListener("touchstart", () => {
isTouchDevice = true
}, { once: true })
element.addEventListener("click", (event) => {
if (isTouchDevice) {
// 仅在非触摸设备上处理
return
}
handleClick(event)
})
// 或使用 pointerdown 事件统一处理
element.addEventListener("pointerdown", (event) => {
if (event.pointerType === "touch") {
// 触摸处理
} else {
// 鼠标处理
}
})事件性能优化
节流(Throttle)
限制事件触发的频率,确保在指定时间间隔内只执行一次。
function throttle(fn, delay) {
let lastTime = 0
let timer = null
return function (...args) {
const now = Date.now()
const remaining = delay - (now - lastTime)
// 首次调用立即执行
if (remaining <= 0) {
clearTimeout(timer)
lastTime = now
fn.apply(this, args)
} else if (!timer) {
// 在剩余时间结束后执行最后一次(trailing)
timer = setTimeout(() => {
lastTime = Date.now()
timer = null
fn.apply(this, args)
}, remaining)
}
}
}防抖(Debounce)
在事件停止触发后执行,避免频繁触发。
function debounce(fn, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 使用示例
const handleSearch = debounce((query) => {
console.log("搜索:", query)
fetchSearchResults(query)
}, 300)
searchInput.addEventListener("input", (e) => {
handleSearch(e.target.value)
})requestAnimationFrame 优化
// 滚动动画优化
let ticking = false
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
updateLayout()
ticking = false
})
ticking = true
}
})
// 平滑滚动到指定位置
function animateScrollTo(targetY, duration = 500) {
const startY = window.scrollY
const distance = targetY - startY
let startTime = null
function step(timestamp) {
if (!startTime) startTime = timestamp
const progress = Math.min((timestamp - startTime) / duration, 1)
const eased = easeOutCubic(progress)
window.scrollTo(0, startY + distance * eased)
if (progress < 1) {
requestAnimationFrame(step)
}
}
requestAnimationFrame(step)
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3)
}Passive 事件监听器
// 提升滚动性能
element.addEventListener("wheel", handler, { passive: true })
element.addEventListener("touchstart", handler, { passive: true })
element.addEventListener("touchmove", handler, { passive: true })
// passive: true 告诉浏览器监听器不会调用 preventDefault()
// 浏览器可以立即开始滚动,不用等待 JS 执行完成
// 检测 passive 支持
let passiveSupported = false
try {
const options = {
get passive() {
passiveSupported = true
return false
}
}
window.addEventListener("test", null, options)
} catch (e) {}
// 安全使用
element.addEventListener("scroll", handler, passiveSupported ? { passive: true } : false)观察者 API
Intersection Observer
用于检测元素是否进入视口,替代滚动监听实现懒加载、无限滚动等功能。
// 基本用法
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log("元素可见:", entry.target)
// 停止观察
observer.unobserve(entry.target)
}
})
}, {
root: null, // 视口作为根元素
rootMargin: "0px", // 根元素的边距
threshold: 0.1 // 10% 可见时触发
})
// 懒加载:提前 100px 加载图片
const scrollObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadImage(entry.target)
scrollObserver.unobserve(entry.target)
}
})
}, {
rootMargin: "100px"
})
scrollObserver.observe(sentinel)Resize Observer
用于监听元素尺寸变化。
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
console.log(`${entry.target.id} 尺寸: ${width}x${height}`)
// 根据尺寸调整布局
if (width < 600) {
entry.target.classList.add("compact")
} else {
entry.target.classList.remove("compact")
}
}
})
// 观察元素
resizeObserver.observe(document.getElementById("container"))
// 停止观察
resizeObserver.unobserve(element)
resizeObserver.disconnect()Mutation Observer
用于监听 DOM 变化。
const mutationObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "childList") {
console.log("子节点变化:", mutation.addedNodes, mutation.removedNodes)
} else if (mutation.type === "attributes") {
console.log("属性变化:", mutation.attributeName, mutation.oldValue)
} else if (mutation.type === "characterData") {
console.log("文本内容变化:", mutation.oldValue)
}
}
})
// 配置观察选项
mutationObserver.observe(targetElement, {
childList: true, // 观察子节点变化
attributes: true, // 观察属性变化
characterData: true, // 观察文本内容变化
subtree: true, // 观察所有后代节点
attributeOldValue: true, // 记录旧属性值
characterDataOldValue: true, // 记录旧文本值
attributeFilter: ["class", "style"] // 仅观察指定属性
})
// 停止观察
mutationObserver.disconnect()最佳实践
内存管理
// ❌ 错误:忘记移除监听器
function setupHandler() {
const data = fetchData()
element.addEventListener("click", () => {
console.log(data) // 闭包持有 data 引用
})
}
// ✅ 正确:使用具名函数便于移除
function setupHandler() {
const data = fetchData()
function handleClick() {
console.log(data)
// 使用后移除
element.removeEventListener("click", handleClick)
}
element.addEventListener("click", handleClick)
}
// ✅ 正确:使用 once 选项
element.addEventListener("click", handler, { once: true })
// ✅ 正确:使用 AbortController
const controller = new AbortController()
element.addEventListener("click", handler, { signal: controller.signal })
// 清理时
controller.abort()事件委托最佳实践
// ✅ 正确的事件委托
document.querySelector(".list").addEventListener("click", (event) => {
// 使用 closest 处理嵌套元素
const item = event.target.closest(".list-item")
if (item && event.currentTarget.contains(item)) {
const action = event.target.closest("[data-action]")
if (action) {
const actionType = action.dataset.action
handleAction(actionType, item)
}
}
})
// 根据操作类型分发
function handleAction(actionType, item) {
switch (actionType) {
case "delete":
item.remove()
break
case "edit":
makeEditable(item)
break
default:
console.log("未知操作:", actionType)
}
}
// 动态添加列表项(事件委托无需重新绑定)
function addItem(list, text) {
const item = document.createElement("li")
item.className = "list-item"
item.innerHTML = `
<span>${text}</span>
<button class="delete-btn" data-action="delete">删除</button>
`
list.appendChild(item)
}无障碍访问
// ✅ 确保键盘可访问
const dropdown = document.querySelector(".dropdown")
dropdown.addEventListener("click", toggleDropdown)
dropdown.addEventListener("keydown", (event) => {
switch (event.key) {
case "Enter":
case " ":
event.preventDefault()
toggleDropdown()
break
case "Escape":
closeDropdown()
break
}
})
// 打开模态框时聚焦第一个可聚焦元素
function openModal(modal) {
modal.hidden = false
const firstFocusable = modal.querySelector("button, [href], input, select, textarea")
firstFocusable?.focus()
// 焦点陷阱:Tab 在模态框内循环
trapFocus(modal)
}常见问题与调试
常见问题
Q: 为什么 preventDefault() 没有效果?
// 检查事件是否可取消
element.addEventListener("click", (event) => {
if (event.cancelable) {
event.preventDefault()
} else {
console.log("此事件不可取消")
}
})
// passive 监听器不能阻止默认行为
element.addEventListener("wheel", (event) => {
event.preventDefault() // 无效,passive 默认为 true
}, { passive: true })
// 正确做法
element.addEventListener("wheel", (event) => {
event.preventDefault() // 有效
}, { passive: false })Q: 为什么 stopPropagation() 后其他监听器还是触发了?
element.addEventListener("click", (event) => {
event.stopPropagation()
// 只阻止事件传播,不阻止当前元素的其他监听器
})
element.addEventListener("click", () => {
// 这个监听器仍会触发
})
// 阻止所有后续监听器
element.addEventListener("click", (event) => {
event.stopImmediatePropagation()
})Q: 如何区分用户触发和脚本触发的事件?
element.addEventListener("click", (event) => {
if (event.isTrusted) {
console.log("用户触发")
} else {
console.log("脚本触发")
}
})
// 用户点击 -> isTrusted: true
element.click() // -> isTrusted: false调试技巧
// 全局事件监听(调试用)
const eventTypes = ["click", "keydown", "submit", "input"]
eventTypes.forEach((type) => {
document.addEventListener(type, (event) => {
const start = performance.now()
console.log(`[${type}]`, {
target: event.target,
currentTarget: event.currentTarget,
bubbles: event.bubbles,
cancelable: event.cancelable,
defaultPrevented: event.defaultPrevented,
isTrusted: event.isTrusted
})
// 记录事件处理器耗时,超过一帧(16ms)警告
const duration = performance.now() - start
if (duration > 16) { // 超过一帧
console.warn(`事件处理器耗时 ${duration.toFixed(2)}ms`, event.type)
}
})
})getEventListeners 工具函数
// 获取元素上的所有事件监听器(仅 Chrome DevTools 支持)
// 在控制台使用:getEventListeners(element)
// 跨浏览器替代方案
function getEventListeners(element) {
const events = {}
// 获取 jQuery 事件
if (window.jQuery) {
const jqueryEvents = jQuery._data(element, "events")
if (jqueryEvents) {
Object.assign(events, jqueryEvents)
}
}
// 注意:原生 addEventListener 添加的监听器无法直接获取
// 建议在开发环境使用代理模式追踪
return events
}