Skip to content
On this page

JavaScript 事件系统

JavaScript事件系统是实现用户交互和动态行为的核心机制。理解事件的工作原理对于开发响应式Web应用至关重要。

事件类型

鼠标事件

javascript
// click - 鼠标点击(包括触摸屏的点击)
element.addEventListener('click', function(event) {
  console.log('Element clicked');
});

// dblclick - 双击
element.addEventListener('dblclick', function(event) {
  console.log('Element double-clicked');
});

// mousedown - 鼠标按钮按下
element.addEventListener('mousedown', function(event) {
  console.log('Mouse button pressed');
});

// mouseup - 鼠标按钮释放
element.addEventListener('mouseup', function(event) {
  console.log('Mouse button released');
});

// mousemove - 鼠标移动
element.addEventListener('mousemove', function(event) {
  console.log(`Mouse at: ${event.clientX}, ${event.clientY}`);
});

// mouseover - 鼠标移到元素上(会冒泡)
element.addEventListener('mouseover', function(event) {
  console.log('Mouse over element');
});

// mouseout - 鼠标离开元素(会冒泡)
element.addEventListener('mouseout', function(event) {
  console.log('Mouse out of element');
});

// mouseenter - 鼠标进入元素(不会冒泡)
element.addEventListener('mouseenter', function(event) {
  console.log('Mouse entered element');
});

// mouseleave - 鼠标离开元素(不会冒泡)
element.addEventListener('mouseleave', function(event) {
  console.log('Mouse left element');
});

// contextmenu - 右键菜单事件
element.addEventListener('contextmenu', function(event) {
  event.preventDefault(); // 阻止默认右键菜单
  showCustomMenu(event.clientX, event.clientY);
});

键盘事件

javascript
// keydown - 按键按下(包括修饰键)
document.addEventListener('keydown', function(event) {
  console.log(`Key pressed: ${event.key}, Code: ${event.code}`);
  
  // 常用快捷键检测
  if (event.ctrlKey && event.key === 's') {
    event.preventDefault();
    saveDocument();
  }
});

// keyup - 按键释放
document.addEventListener('keyup', function(event) {
  console.log(`Key released: ${event.key}`);
});

// keypress - 按键按下并产生字符(已废弃,建议使用keydown/keyup)
// 注意:keypress不会触发修饰键、功能键等

表单事件

javascript
const form = document.querySelector('form');
const input = document.querySelector('input');

// input - 输入值改变时触发(实时)
input.addEventListener('input', function(event) {
  console.log('Input value:', event.target.value);
});

// change - 元素失去焦点且值改变时触发
input.addEventListener('change', function(event) {
  console.log('Value changed to:', event.target.value);
});

// focus - 元素获得焦点
input.addEventListener('focus', function(event) {
  event.target.style.backgroundColor = '#e6f3ff';
});

// blur - 元素失去焦点
input.addEventListener('blur', function(event) {
  event.target.style.backgroundColor = '';
});

// submit - 表单提交
form.addEventListener('submit', function(event) {
  event.preventDefault(); // 阻止默认提交
  validateAndSubmitForm();
});

// reset - 表单重置
form.addEventListener('reset', function(event) {
  console.log('Form reset');
});

窗口事件

javascript
// load - 页面完全加载完成
window.addEventListener('load', function() {
  console.log('Page fully loaded');
});

// DOMContentLoaded - DOM构建完成(不等待图片等资源)
document.addEventListener('DOMContentLoaded', function() {
  console.log('DOM ready');
});

// resize - 窗口大小改变
window.addEventListener('resize', function() {
  console.log(`Window size: ${window.innerWidth} x ${window.innerHeight}`);
});

// scroll - 滚动事件
window.addEventListener('scroll', function() {
  console.log(`Scroll position: ${window.scrollY}`);
});

// beforeunload - 页面即将卸载
window.addEventListener('beforeunload', function(event) {
  event.preventDefault();
  event.returnValue = 'Are you sure you want to leave?';
});

触摸事件(移动端)

javascript
// touchstart - 触摸开始
element.addEventListener('touchstart', function(event) {
  const touch = event.touches[0];
  console.log(`Touch started at: ${touch.clientX}, ${touch.clientY}`);
});

// touchmove - 触摸移动
element.addEventListener('touchmove', function(event) {
  event.preventDefault(); // 阻止页面滚动
  const touch = event.touches[0];
  console.log(`Touch moved to: ${touch.clientX}, ${touch.clientY}`);
});

// touchend - 触摸结束
element.addEventListener('touchend', function(event) {
  console.log('Touch ended');
});

事件处理方式

HTML事件处理程序(不推荐)

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

<!-- 调用全局函数 -->
<button onclick="handleClick()">Click me</button>

DOM0级事件处理程序

javascript
const button = document.querySelector('button');

// 直接赋值事件处理函数
button.onclick = function() {
  console.log('Button clicked');
};

// 也可以赋值函数名
function handleClick() {
  console.log('Button clicked');
}
button.onclick = handleClick;

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

DOM2级事件处理程序(推荐)

javascript
const button = document.querySelector('button');

// 添加事件监听器
function handleClick() {
  console.log('Button clicked');
}

button.addEventListener('click', handleClick);

// 移除事件监听器(必须使用相同的函数引用)
button.removeEventListener('click', handleClick);

// 可以添加多个相同的事件监听器
button.addEventListener('click', function() {
  console.log('First handler');
});
button.addEventListener('click', function() {
  console.log('Second handler');
});
// 两个处理器都会执行

事件监听器选项

javascript
element.addEventListener('click', handler, {
  once: true,        // 只执行一次
  passive: true,     // 被动模式,不能调用preventDefault
  capture: true      // 在捕获阶段执行
});

// 简写形式(布尔值)
element.addEventListener('click', handler, true);  // 捕获阶段
element.addEventListener('click', handler, false); // 冒泡阶段(默认)

事件对象详解

每个事件处理器都会接收一个事件对象,包含事件的详细信息:

javascript
element.addEventListener('click', function(event) {
  // 事件基本信息
  console.log('Event type:', event.type);           // 'click'
  console.log('Target element:', event.target);     // 触发事件的实际元素
  console.log('Current target:', event.currentTarget); // 当前处理事件的元素
  
  // 事件阶段
  console.log('Event phase:', event.eventPhase);    // 0:无, 1:捕获, 2:目标, 3:冒泡
  
  // 鼠标信息
  console.log('Client coordinates:', event.clientX, event.clientY);
  console.log('Page coordinates:', event.pageX, event.pageY);
  console.log('Screen coordinates:', event.screenX, event.screenY);
  console.log('Button pressed:', event.button);     // 0:左键, 1:中键, 2:右键
  
  // 键盘信息
  console.log('Key pressed:', event.key);
  console.log('Key code:', event.code);
  console.log('Ctrl key:', event.ctrlKey);
  console.log('Shift key:', event.shiftKey);
  console.log('Alt key:', event.altKey);
  console.log('Meta key (Cmd/Win):', event.metaKey);
  
  // 事件流控制
  event.preventDefault();     // 阻止默认行为
  event.stopPropagation();    // 阻止事件冒泡
  event.stopImmediatePropagation(); // 阻止其他监听器执行
});

事件阶段与事件流

事件捕获与冒泡

javascript
// HTML结构: <div id="outer"><div id="inner">Click me</div></div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');

// 捕获阶段监听器
outer.addEventListener('click', function() {
  console.log('Outer - capture phase');
}, true); // 第三个参数为true表示捕获阶段

inner.addEventListener('click', function() {
  console.log('Inner - target phase');
}, true); // 仍为捕获阶段

// 冒泡阶段监听器(默认)
inner.addEventListener('click', function() {
  console.log('Inner - bubble phase');
}, false); // 第三个参数为false或省略表示冒泡阶段

outer.addEventListener('click', function() {
  console.log('Outer - bubble phase');
}, false);

// 点击内部div时的输出顺序:
// 1. "Outer - capture phase"
// 2. "Inner - target phase" (捕获阶段)
// 3. "Inner - bubble phase" (目标阶段,冒泡阶段)
// 4. "Outer - bubble phase" (冒泡阶段)

事件委托实现

javascript
// 利用事件冒泡机制,在父元素上处理子元素事件
document.querySelector('#list-container').addEventListener('click', function(event) {
  // 使用事件委托处理动态添加的元素
  if (event.target.matches('.list-item')) {
    console.log('List item clicked:', event.target.textContent);
  }
  
  if (event.target.matches('.delete-btn')) {
    event.target.closest('.list-item').remove();
  }
  
  if (event.target.matches('a')) {
    event.preventDefault(); // 阻止链接跳转
    console.log('Link clicked:', event.target.href);
  }
});

// 即使后续添加新的列表项,也能正常工作
const newItem = document.createElement('div');
newItem.className = 'list-item';
newItem.textContent = 'Dynamically added item';
document.querySelector('#list-container').appendChild(newItem);
// 点击新添加的项也会触发上面的事件处理器

自定义事件

创建和分发自定义事件

javascript
// 创建自定义事件
const customEvent = new CustomEvent('myCustomEvent', {
  detail: { 
    message: 'Hello from custom event!',
    data: { userId: 123, action: 'click' }
  },
  bubbles: true,    // 是否冒泡
  cancelable: true  // 是否可以取消
});

// 监听自定义事件
element.addEventListener('myCustomEvent', function(event) {
  console.log('Custom event received:', event.detail);
  console.log('Message:', event.detail.message);
  console.log('Data:', event.detail.data);
});

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

// 简单的自定义事件
const simpleEvent = new Event('simpleEvent');
element.addEventListener('simpleEvent', () => console.log('Simple event fired'));
element.dispatchEvent(simpleEvent);

自定义事件的实际应用

javascript
// 组件间通信
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }
  
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
  
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}

// 使用事件发射器
const emitter = new EventEmitter();
emitter.on('userLogin', (userData) => {
  console.log('User logged in:', userData);
});
emitter.emit('userLogin', { id: 1, name: 'John' });

事件性能优化

防抖(Debounce)

javascript
// 防止函数在短时间内被频繁调用
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

// 应用于搜索输入
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce(function(event) {
  performSearch(event.target.value);
}, 300);

searchInput.addEventListener('input', debouncedSearch);

节流(Throttle)

javascript
// 限制函数在指定时间间隔内最多执行一次
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// 应用于滚动事件
const throttledScroll = throttle(function() {
  console.log('Scrolling...', window.scrollY);
}, 100);

window.addEventListener('scroll', throttledScroll);

事件委托优化

javascript
// 不好的做法:为每个按钮添加事件监听器
document.querySelectorAll('.button').forEach(button => {
  button.addEventListener('click', handleClick);
});

// 好的做法:使用事件委托
document.addEventListener('click', function(event) {
  if (event.target.matches('.button')) {
    handleClick(event);
  }
});

// 进一步优化:缓存选择器结果
const container = document.querySelector('#button-container');
container.addEventListener('click', function(event) {
  if (event.target.matches('.button')) {
    handleClick(event);
  }
});

现代事件处理模式

使用AbortController移除事件监听器

javascript
// ES2021+ 的新方式
const controller = new AbortController();

element.addEventListener('click', function() {
  console.log('Clicked!');
}, { signal: controller.signal });

// 在需要时移除所有相关监听器
controller.abort();

事件处理器类

javascript
class EventHandler {
  constructor() {
    this.eventMap = new Map();
  }
  
  add(element, event, handler, options) {
    element.addEventListener(event, handler, options);
    
    // 记录事件以便后续清理
    const key = `${element.tagName}-${event}`;
    if (!this.eventMap.has(key)) {
      this.eventMap.set(key, []);
    }
    this.eventMap.get(key).push({ element, event, handler, options });
  }
  
  removeAll() {
    for (const [key, events] of this.eventMap) {
      events.forEach(({ element, event, handler }) => {
        element.removeEventListener(event, handler);
      });
    }
    this.eventMap.clear();
  }
}

// 使用事件处理器
const eventHandler = new EventHandler();
eventHandler.add(button, 'click', handleClick);
// 页面卸载时清理所有事件
window.addEventListener('beforeunload', () => eventHandler.removeAll());

常见事件处理陷阱

this绑定问题

javascript
class Component {
  constructor(element) {
    this.element = element;
    // 错误:this指向不正确
    // this.element.addEventListener('click', this.handleClick);
    
    // 解决方案1:使用箭头函数
    this.element.addEventListener('click', () => this.handleClick());
    
    // 解决方案2:绑定this
    this.element.addEventListener('click', this.handleClick.bind(this));
    
    // 解决方案3:预先绑定
    this.boundHandleClick = this.handleClick.bind(this);
    this.element.addEventListener('click', this.boundHandleClick);
  }
  
  handleClick() {
    console.log('Component clicked', this); // 正确的this
  }
}

内存泄漏预防

javascript
class Component {
  constructor(element) {
    this.element = element;
    this.boundClickHandler = this.handleClick.bind(this);
    this.boundScrollHandler = this.handleScroll.bind(this);
    
    this.element.addEventListener('click', this.boundClickHandler);
    window.addEventListener('scroll', this.boundScrollHandler);
  }
  
  destroy() {
    // 移除事件监听器防止内存泄漏
    this.element.removeEventListener('click', this.boundClickHandler);
    window.removeEventListener('scroll', this.boundScrollHandler);
    
    // 清理引用
    this.element = null;
    this.boundClickHandler = null;
    this.boundScrollHandler = null;
  }
  
  handleClick() {
    // 处理点击
  }
  
  handleScroll() {
    // 处理滚动
  }
}

JavaScript事件系统是构建交互式Web应用的基础,理解其工作原理和最佳实践对于开发高质量的应用至关重要。