事件

事件是 JavaScript 与 HTML 交互的基础,允许页面响应用户操作和浏览器状态变化。

事件流

事件流描述了页面中接收事件的顺序。

事件冒泡

<!-- HTML 结构 -->
<div id="outer">
  <div id="inner">
    <button id="button">Click me</button>
  </div>
</div>
// 事件冒泡:从最具体的元素开始,向上传播
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
const button = document.getElementById('button')

button.addEventListener('click', () => console.log('Button clicked'))
inner.addEventListener('click', () => console.log('Inner div clicked'))
outer.addEventListener('click', () => console.log('Outer div clicked'))

// 点击按钮输出:
// Button clicked
// Inner div clicked
// Outer div clicked

事件捕获

// 事件捕获:从最外层元素开始,向下传播
button.addEventListener('click', () => console.log('Button clicked'), true)
inner.addEventListener('click', () => console.log('Inner div clicked'), true)
outer.addEventListener('click', () => console.log('Outer div clicked'), true)

// 点击按钮输出:
// Outer div clicked
// Inner div clicked
// Button clicked

DOM 事件流

DOM2 Events 规范规定事件流包含三个阶段:

  1. 捕获阶段:事件从 window 开始向下传播到目标元素
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:事件从目标元素向上冒泡回 window
// 同时监听捕获和冒泡
button.addEventListener('click', () => console.log('Button - capture'), true)
button.addEventListener('click', () => console.log('Button - bubble'), false)
inner.addEventListener('click', () => console.log('Inner - capture'), true)
inner.addEventListener('click', () => console.log('Inner - bubble'), false)

// 点击按钮输出:
// Inner - capture
// Button - capture
// Button - bubble
// Inner - bubble

事件处理程序

添加事件处理程序的方法。

HTML 事件处理程序

<!-- 不推荐:HTML 内联事件处理程序 -->
<button onclick="alert('Hello')">Click me</button>

<!-- 访问元素和事件对象 -->
<button onclick="console.log(this.tagName); console.log(event.type)">Click me</button>

DOM0 事件处理程序

const button = document.getElementById('button')

// 添加事件处理程序
button.onclick = function(event) {
  console.log('Button clicked')
  console.log(this === button) // true
}

// 移除事件处理程序
button.onclick = null

DOM2 事件处理程序

const button = document.getElementById('button')

// 添加事件处理程序
function handleClick(event) {
  console.log('Button clicked')
  console.log(this === button) // true
}

button.addEventListener('click', handleClick, false)

// 添加多个处理程序
button.addEventListener('click', function() {
  console.log('Another handler')
}, false)

// 移除事件处理程序
button.removeEventListener('click', handleClick, false)

// 一次性事件处理程序
button.addEventListener('click', function handler() {
  console.log('This will only run once')
  button.removeEventListener('click', handler)
})

事件对象

事件触发时会创建 event 对象,包含事件相关信息。

事件对象属性

button.addEventListener('click', function(event) {
  // 事件基本信息
  console.log(event.type)        // 事件类型,如 'click'
  console.log(event.target)      // 事件目标
  console.log(event.currentTarget) // 当前事件处理程序绑定的元素

  // 鼠标事件位置
  console.log(event.clientX, event.clientY) // 视口坐标
  console.log(event.pageX, event.pageY)     // 页面坐标
  console.log(event.screenX, event.screenY) // 屏幕坐标

  // 键盘事件
  console.log(event.key)         // 按键值
  console.log(event.keyCode)     // 按键码(已废弃)
  console.log(event.code)        // 按键代码

  // 修饰键
  console.log(event.ctrlKey)     // Ctrl 键是否按下
  console.log(event.altKey)      // Alt 键是否按下
  console.log(event.shiftKey)    // Shift 键是否按下
  console.log(event.metaKey)     // Meta 键是否按下(Cmd 或 Windows 键)
})

事件对象方法

button.addEventListener('click', function(event) {
  // 阻止默认行为
  event.preventDefault()

  // 停止事件传播
  event.stopPropagation()

  // 立即停止事件传播(包括同一元素其他处理程序)
  event.stopImmediatePropagation()
})

事件类型

UI 事件

// 加载事件
window.addEventListener('load', () => {
  console.log('Page loaded')
})

document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM ready')
})

// 卸载事件
window.addEventListener('beforeunload', (event) => {
  event.returnValue = 'Are you sure you want to leave?'
})

window.addEventListener('unload', () => {
  console.log('Page unloading')
})

// 调整大小事件
window.addEventListener('resize', () => {
  console.log(`Window size: ${window.innerWidth}x${window.innerHeight}`)
})

// 滚动事件
window.addEventListener('scroll', () => {
  console.log(`Scroll position: ${window.pageXOffset}, ${window.pageYOffset}`)
})

鼠标事件

const element = document.getElementById('element')

// 基本鼠标事件
element.addEventListener('click', handleClick)
element.addEventListener('dblclick', handleDoubleClick)
element.addEventListener('mousedown', handleMouseDown)
element.addEventListener('mouseup', handleMouseUp)
element.addEventListener('mousemove', handleMouseMove)

// 鼠标按键信息
function handleMouseDown(event) {
  switch (event.button) {
    case 0:
      console.log('Left button')
      break
    case 1:
      console.log('Middle button')
      break
    case 2:
      console.log('Right button')
      break
  }
}

// 滚轮事件
element.addEventListener('wheel', (event) => {
  console.log(`Delta: ${event.deltaX}, ${event.deltaY}, ${event.deltaZ}`)
  console.log(`Mode: ${event.deltaMode}`) // 0: 像素, 1: 行, 2: 页
})

键盘事件

const input = document.getElementById('input')

// 键盘事件
input.addEventListener('keydown', (event) => {
  console.log(`Key down: ${event.key}, code: ${event.code}`)
})

input.addEventListener('keypress', (event) => {
  console.log(`Key press: ${event.key}`)
})

input.addEventListener('keyup', (event) => {
  console.log(`Key up: ${event.key}`)
})

// 组合键检测
input.addEventListener('keydown', (event) => {
  if (event.ctrlKey && event.key === 's') {
    event.preventDefault()
    console.log('Ctrl+S pressed - saving...')
  }
})

表单事件

const form = document.getElementById('form')
const input = document.getElementById('input')

// 表单事件
form.addEventListener('submit', (event) => {
  event.preventDefault()
  console.log('Form submitted')
})

form.addEventListener('reset', () => {
  console.log('Form reset')
})

// 输入事件
input.addEventListener('focus', () => {
  console.log('Input focused')
})

input.addEventListener('blur', () => {
  console.log('Input blurred')
})

input.addEventListener('change', () => {
  console.log('Input value changed')
})

input.addEventListener('input', () => {
  console.log('Input value changing')
})

剪贴板事件

const textArea = document.getElementById('textArea')

textArea.addEventListener('copy', (event) => {
  console.log('Content copied')
  // 可以修改剪贴板内容
  event.clipboardData.setData('text/plain', 'Modified text')
  event.preventDefault()
})

textArea.addEventListener('paste', (event) => {
  console.log('Content pasted')
  const pastedText = event.clipboardData.getData('text/plain')
  console.log('Pasted:', pastedText)
})

textArea.addEventListener('cut', () => {
  console.log('Content cut')
})

事件委托

利用事件冒泡,将事件处理程序添加到祖先元素。

// 不推荐:为每个列表项添加事件处理程序
const items = document.querySelectorAll('.item')
items.forEach(item => {
  item.addEventListener('click', handleItemClick)
})

// 推荐:事件委托
const list = document.getElementById('list')
list.addEventListener('click', (event) => {
  if (event.target.matches('.item')) {
    handleItemClick(event)
  }
})

// 更复杂的委托
list.addEventListener('click', (event) => {
  const target = event.target

  if (target.matches('.delete-btn')) {
    deleteItem(target.dataset.id)
  } else if (target.matches('.edit-btn')) {
    editItem(target.dataset.id)
  } else if (target.matches('.item')) {
    selectItem(target.dataset.id)
  }
})

自定义事件

创建和触发自定义事件。

// 创建自定义事件
const customEvent = new CustomEvent('myEvent', {
  detail: { message: 'Hello from custom event' },
  bubbles: true,    // 是否冒泡
  cancelable: true  // 是否可以取消
})

// 监听自定义事件
element.addEventListener('myEvent', (event) => {
  console.log(event.detail.message)
})

// 触发自定义事件
element.dispatchEvent(customEvent)

// 兼容性处理
function createCustomEvent(eventName, detail) {
  let event

  if (typeof CustomEvent === 'function') {
    event = new CustomEvent(eventName, { detail })
  } else {
    event = document.createEvent('CustomEvent')
    event.initCustomEvent(eventName, true, true, detail)
  }

  return event
}

内存和性能

移除事件处理程序

// 在不需要时移除事件处理程序
const button = document.createElement('button')
const handler = () => console.log('Clicked')

button.addEventListener('click', handler)

// 移除事件处理程序
button.removeEventListener('click', handler)

// 在元素被移除前移除事件处理程序
function removeElement(element) {
  // 移除所有事件处理程序
  element.parentNode.removeChild(element)
}

事件节流和防抖

// 节流:限制函数执行频率
function throttle(func, delay) {
  let lastCall = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastCall >= delay) {
      lastCall = now
      func.apply(this, args)
    }
  }
}

// 防抖:延迟执行,直到停止触发
function debounce(func, delay) {
  let timeoutId
  return function(...args) {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => func.apply(this, args), delay)
  }
}

// 使用示例
window.addEventListener('resize', debounce(handleResize, 250))
window.addEventListener('scroll', throttle(handleScroll, 16))

总结

事件是 JavaScript 编程的核心概念:

  1. 事件流:包括捕获阶段、目标阶段和冒泡阶段
  2. 事件处理程序:HTML 内联、DOM0、DOM2 三种添加方式
  3. 事件对象:包含事件详细信息和控制方法
  4. 事件类型:UI、鼠标、键盘、表单、剪贴板等多种事件
  5. 事件委托:利用冒泡提高性能
  6. 自定义事件:创建和触发自定义事件

合理使用事件可以创建丰富的用户交互体验。