{T}

事件类型

Web 平台事件体系非常庞杂,除了 DOM Level 3 Events 定义的标准事件,HTML 规范、浏览器扩展以及多端设备也带来了更多补充。为了在项目中选择合适的事件,需要理解它们的分类、触发时机以及常用属性。

[[TOC]]

事件分类速览

分类典型场景常见事件举例
UI(用户界面)页面结构/资源加载、滚动、尺寸变化loadDOMContentLoadedresizescroll
焦点元素获得/失去焦点、可访问性提示focusblurfocusinfocusout
鼠标与滚轮光标移动、按钮点击、滚轮滑动clickmousedownmousemovewheel
键盘与输入键盘交互、文本输入、剪贴板、IMEkeydownkeyupinputcompositionend
指针事件鼠标、触摸、笔统一输入pointerdownpointermovepointercancel
拖放元素拖拽、文件拖放dragstartdragoverdrop
表单表单提交、重置、值变化submitresetchangeinput
剪贴板复制、剪切、粘贴copycutpaste
多媒体音视频播放控制playpauseendedtimeupdate
动画CSS 动画与过渡animationstarttransitionend
网络在线状态、网络请求onlineofflinefetch
存储本地存储变化监听storageindexedDB 事务
设备与传感器设备方向、加速度、环境光线、屏幕方向orientationchangedeviceorientationdevicemotion
触摸与手势多点触控、手势缩放/旋转touchstarttouchmovegesturechange
全屏全屏模式切换fullscreenchangefullscreenerror
挂起与可见性页面可见性、页面生命周期visibilitychangepagehidepageshow

DOM3 EventsDOM2 Events 基础上重新定义了事件系统,主流浏览器均已支持。HTML Living Standard、Pointer Events 等规范在此基础上不断扩展。

DOM 事件模型概览

事件传播机制

code
捕获阶段(Capture Phase) → 目标阶段(Target Phase) → 冒泡阶段(Bubble Phase)
        document ↓                        ↓                         ↑ document
           ↓                            target                      ↑
          body ↓                          ↓                         ↑ body
           ↓                            元素                        ↑
         元素  ←←←←←←←←←←←←←←←←←←←←←←←←←←←←
  • 捕获阶段:事件从 document 向下传播到目标元素
  • 目标阶段:事件到达目标元素
  • 冒泡阶段:事件从目标元素向上冒泡到 document(默认监听阶段)
javascript
// 在捕获阶段触发
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()阻止事件传播并阻止同一元素上的其他监听器

事件委托

事件委托利用事件冒泡机制,在父元素上监听子元素的事件,减少监听器数量并支持动态元素。

javascript
// ❌ 不推荐:为每个元素单独绑定
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)
  }
})

监听器选项

javascript
// 完整选项对象
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()

自定义事件

javascript
// 创建自定义事件
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 事件速查表

事件触发节点说明与现代建议
loadwindow、资源元素资源全部加载后触发,SPA 更常使用 DOMContentLoaded 或资源级 load
DOMContentLoadeddocumentDOM 解析完成后触发,不等待样式表、图片等资源
unloadwindow页面卸载后触发,现代浏览器多限制其行为,建议转向 pagehide/visibilitychange
beforeunloadwindow页面卸载前触发,可提示用户确认
abort<object>资源加载被用户取消时触发,现代场景较少用
errorwindow、资源元素捕获加载失败与脚本错误;脚本可通过 addEventListener("error", handler, true) 捕获
select可输入控件文本被选中时触发,常用于同步输入与辅助提示
resizewindow窗口尺寸变更;对于元素尺寸变化,使用 ResizeObserver
scrollwindow、可滚动容器滚动位置变化,高频事件需配合节流、防抖或 requestAnimationFrame

load 事件

load 事件在所有资源(包括图片、样式表、脚本等)加载完成后触发。

javascript
// 监听页面完全加载
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 解析完成后立即触发,不等待样式表、图片等外部资源。

javascript
document.addEventListener("DOMContentLoaded", (event) => {
  console.log("DOM 已就绪,可以安全操作 DOM")
  // 此时可以访问和操作 DOM 元素
  const app = document.getElementById("app")
  app.innerHTML = "<p>应用已初始化</p>"
})

执行顺序对比:

javascript
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 触发,推荐使用 pagehidevisibilitychange 或持久化机制(如 navigator.sendBeacon)。

javascript
// ❌ 不推荐: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 事件

用于在用户离开页面前提示确认。

javascript
let hasUnsavedChanges = false

// 标记有未保存的更改
function markUnsaved() {
  hasUnsavedChanges = true
}

window.addEventListener("beforeunload", (event) => {
  if (hasUnsavedChanges) {
    // 现代浏览器会忽略自定义消息,只显示默认提示
    event.preventDefault()
    event.returnValue = "" // Chrome 需要这个
    return "" // 旧版浏览器需要这个
  }
})

// 用户保存后清除标记
function saveChanges() {
  // ... 保存逻辑
  hasUnsavedChanges = false
}

⚠️ 注意事项

  • 现代浏览器出于防骚扰考虑,只在用户交互后触发对话框
  • 自定义文案会被忽略,只显示浏览器默认提示
  • 不要滥用此事件,影响用户体验

resize 事件

javascript
// 监听窗口尺寸变化
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 事件

javascript
// 获取滚动位置(跨浏览器兼容)
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 的冒泡版本)

基本用法

javascript
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")
  }
})

获取焦点信息

javascript
// 当前获得焦点的元素
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)
})

焦点管理

javascript
// 编程方式设置焦点
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右键点击时触发,用于自定义右键菜单

事件触发顺序

单击:

code
mousedown → mouseup → click

双击:

code
mousedown → mouseup → click → mousedown → mouseup → click → dblclick

鼠标移入移出:

code
mouseover → mouseenter → (mousemove...) → mouseout → mouseleave

鼠标事件对象属性

javascript
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)
})

坐标系统示意图

code
┌─────────────────────────────────────────────────────────────┐
│                        Screen (screenX/Y)                   │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                  Browser Window                      │   │
│   │   ┌─────────────────────────────────────────────┐   │   │
│   │   │           Viewport (clientX/Y)               │   │   │
│   │   │                                              │   │   │
│   │   │    ┌─────────────────────┐                  │   │   │
│   │   │    │   Element           │                  │   │   │
│   │   │    │   (offsetX/Y)       │                  │   │   │
│   │   │    │          •click     │                  │   │   │
│   │   │    └─────────────────────┘                  │   │   │
│   │   │                                              │   │   │
│   │   └─────────────────────────────────────────────┘   │   │
│   │                                                      │   │
│   └──────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘
                          Document (pageX/Y)

mouseenter/mouseleave vs mouseover/mouseout

javascript
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)
})

滚轮事件

javascript
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 才能阻止默认行为

自定义右键菜单

javascript
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 的核心优势:

  1. 统一输入模型:一套 API 处理鼠标、触摸、笔等所有输入设备
  2. 支持多点触控:通过 pointerId 区分不同的触控点
  3. 支持笔输入:包含压力、倾斜等特有属性
  4. 自动处理兼容性:浏览器会自动将触摸转换为鼠标事件

指针事件对象属性

code
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)
})

指针捕获

指针捕获允许特定元素捕获指针事件,即使指针移出元素边界,事件仍会派发到该元素:

javascript
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)
})

指针事件与触摸事件对比

javascript
// ❌ 传统方式:分别监听鼠标和触摸
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
  }
}

多点触控处理

javascript
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在目标元素上放下时目标元素

基本拖放示例

html
<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 对象

javascript
// 设置数据
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

文件拖放

javascript
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)
      }
    }
  })
})

拖放排序示例

javascript
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释放键盘上的键时触发

键盘事件对象属性

javascript
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 的区别

javascript
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 键

快捷键实现

javascript
// 全局快捷键
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)
})

无障碍访问考虑

javascript
// ✅ 正确:同时支持键盘和鼠标操作
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 元素值发生变化时触发。

javascript
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 事件在输入实际发生之前触发,可以取消输入操作:

javascript
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(输入法编辑器)的输入序列,如中文输入。

javascript
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)
  }
})

实时搜索示例

html
<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 事件

html
<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 事件在表单控件的值发生变化并失去焦点时触发。

javascript
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 的区别

javascript
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 事件会立即触发

表单验证

javascript
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粘贴操作时触发

监听剪贴板事件

javascript
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

javascript
// 写入剪贴板
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 在部分浏览器中可能不完全支持,需要特性检测
javascript
// 特性检测与降级
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发生错误时触发

基本使用

code
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)
})

自定义视频播放器

javascript
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 动画事件

事件说明
animationstartCSS 动画开始时触发
animationendCSS 动画结束时触发
animationiterationCSS 动画每次迭代时触发
javascript
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 过渡事件

事件说明
transitionstartCSS 过渡开始时触发
transitionendCSS 过渡结束时触发
transitionrunCSS 过渡创建时触发
transitioncancelCSS 过渡取消时触发
javascript
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

javascript
// 使用 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 事件

javascript
// 监听网络状态变化
window.addEventListener("online", () => {
  console.log("网络已连接")
  showNotification("网络已恢复")
  syncData()
})

window.addEventListener("offline", () => {
  console.log("网络已断开")
  showNotification("网络已断开,部分功能可能不可用")
})

// 检查当前网络状态
if (navigator.onLine) {
  console.log("当前在线")
} else {
  console.log("当前离线")
}

Network Information API

javascript
// 获取网络信息
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 事件

localStoragesessionStorage 被修改时,在其他同源页面触发:

javascript
// 监听存储变化(在其他标签页/窗口触发)
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 事件

javascript
// 打开数据库
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 })
  }
})

全屏事件

javascript
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 事件

javascript
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 事件

javascript
// 页面显示(包括从 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")
    // 清理状态,避免恢复时出现问题
  }
})

设备与传感器事件

设备方向事件

javascript
// 监听设备方向
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)`
})

设备运动事件

javascript
// 监听设备运动
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()
    }
  }
})

屏幕方向事件

javascript
// 监听屏幕方向变化
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系统停止跟踪触摸时触发

触摸事件对象

javascript
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)
})

多点触控示例

javascript
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)

javascript
// 手势事件(仅 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("手势结束")
})

触摸与鼠标事件顺序

code
touchstart
touchend
mouseover
mousemove (一次)
mousedown
mouseup
click
javascript
// 避免触摸设备上触发鼠标事件
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)

限制事件触发的频率,确保在指定时间间隔内只执行一次。

javascript
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)

在事件停止触发后执行,避免频繁触发。

javascript
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 优化

javascript
// 滚动动画优化
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 事件监听器

javascript
// 提升滚动性能
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

用于检测元素是否进入视口,替代滚动监听实现懒加载、无限滚动等功能。

javascript
// 基本用法
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

用于监听元素尺寸变化。

javascript
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 变化。

javascript
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()

最佳实践

内存管理

javascript
// ❌ 错误:忘记移除监听器
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()

事件委托最佳实践

javascript
// ✅ 正确的事件委托
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)
}

无障碍访问

javascript
// ✅ 确保键盘可访问
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() 没有效果?

javascript
// 检查事件是否可取消
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() 后其他监听器还是触发了?

javascript
element.addEventListener("click", (event) => {
  event.stopPropagation()
  // 只阻止事件传播,不阻止当前元素的其他监听器
})

element.addEventListener("click", () => {
  // 这个监听器仍会触发
})

// 阻止所有后续监听器
element.addEventListener("click", (event) => {
  event.stopImmediatePropagation()
})

Q: 如何区分用户触发和脚本触发的事件?

javascript
element.addEventListener("click", (event) => {
  if (event.isTrusted) {
    console.log("用户触发")
  } else {
    console.log("脚本触发")
  }
})

// 用户点击 -> isTrusted: true
element.click() // -> isTrusted: false

调试技巧

javascript
// 全局事件监听(调试用)
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 工具函数

javascript
// 获取元素上的所有事件监听器(仅 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
}

参考资源