阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 事件监听模式

事件监听模式

作者:陈川 阅读数:50717人阅读 分类: JavaScript

事件监听模式

事件监听模式是JavaScript中处理用户交互的核心机制。它允许开发者在特定事件发生时执行预定义的函数,从而实现动态响应。浏览器环境中的点击、滚动、键盘输入等行为都依赖这种模式。

事件监听基础

DOM元素通过addEventListener方法注册事件处理器:

const button = document.querySelector('#myButton');

button.addEventListener('click', function(event) {
  console.log('按钮被点击', event.target);
});

这个方法接收三个参数:

  1. 事件类型(如'click')
  2. 事件触发时执行的回调函数
  3. 可选的配置对象

事件传播机制

事件在DOM树中经历三个阶段:

  1. 捕获阶段:从window对象向下传播到目标元素
  2. 目标阶段:到达事件目标元素
  3. 冒泡阶段:从目标元素向上冒泡到window
document.querySelector('.outer').addEventListener('click', () => {
  console.log('捕获阶段', true);
}, true);

document.querySelector('.inner').addEventListener('click', (event) => {
  console.log('目标阶段');
  event.stopPropagation(); // 阻止事件传播
});

document.body.addEventListener('click', () => {
  console.log('冒泡阶段');
});

事件委托模式

利用事件冒泡机制,可以在父元素上统一处理子元素的事件:

document.querySelector('#list').addEventListener('click', (event) => {
  if (event.target.matches('li')) {
    console.log('点击了列表项:', event.target.textContent);
  }
});

这种模式特别适合动态生成的元素,无需为每个子元素单独绑定事件。

自定义事件

JavaScript允许创建和触发自定义事件:

// 创建事件
const customEvent = new CustomEvent('build', {
  detail: { time: new Date() },
  bubbles: true,
  cancelable: true
});

// 监听事件
document.addEventListener('build', (e) => {
  console.log('自定义事件触发:', e.detail);
});

// 触发事件
document.dispatchEvent(customEvent);

异步事件处理

现代JavaScript引入了异步事件处理模式:

const controller = new AbortController();

// 添加可取消的事件监听
button.addEventListener('click', async () => {
  const data = await fetchData();
  console.log(data);
}, { signal: controller.signal });

// 取消所有相关事件监听
controller.abort();

性能优化技巧

  1. 防抖:确保事件处理器在快速连续触发时只执行一次
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

window.addEventListener('resize', debounce(() => {
  console.log('窗口大小调整结束');
}, 200));
  1. 节流:限制事件处理器的执行频率
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

document.addEventListener('scroll', throttle(() => {
  console.log('滚动事件处理');
}, 100));

跨浏览器兼容性

虽然现代浏览器基本遵循标准,但旧版本IE有不同实现:

function addEvent(element, type, handler) {
  if (element.addEventListener) {
    element.addEventListener(type, handler, false);
  } else if (element.attachEvent) {
    element.attachEvent('on' + type, handler);
  } else {
    element['on' + type] = handler;
  }
}

事件对象详解

事件回调函数接收的事件对象包含丰富信息:

document.addEventListener('click', (event) => {
  console.log('坐标:', event.clientX, event.clientY);
  console.log('触发元素:', event.target);
  console.log('当前元素:', event.currentTarget);
  console.log('事件阶段:', event.eventPhase);
  
  // 阻止默认行为
  event.preventDefault();
  
  // 阻止事件传播
  event.stopPropagation();
  
  // 立即停止所有处理
  event.stopImmediatePropagation();
});

现代框架中的事件处理

React等框架封装了原生事件系统:

function MyComponent() {
  const handleClick = useCallback((event) => {
    console.log('React合成事件:', event.nativeEvent);
  }, []);

  return <button onClick={handleClick}>点击</button>;
}

Vue提供了更简洁的语法:

<template>
  <button @click="handleClick($event)">点击</button>
</template>

<script>
export default {
  methods: {
    handleClick(event) {
      console.log('Vue事件对象:', event);
    }
  }
}
</script>

触摸和手势事件

移动端设备支持特殊事件类型:

const touchArea = document.getElementById('touch-area');

touchArea.addEventListener('touchstart', (e) => {
  const touch = e.touches[0];
  console.log('触摸开始:', touch.clientX, touch.clientY);
});

touchArea.addEventListener('touchmove', (e) => {
  e.preventDefault(); // 阻止滚动
  console.log('触摸移动');
});

touchArea.addEventListener('touchend', () => {
  console.log('触摸结束');
});

键盘事件深度处理

键盘事件可以精确控制用户输入:

document.addEventListener('keydown', (event) => {
  console.log('键码:', event.keyCode);
  console.log('按键:', event.key);
  
  // Ctrl + S 组合键
  if (event.ctrlKey && event.key === 's') {
    event.preventDefault();
    saveContent();
  }
});

function saveContent() {
  console.log('保存操作被触发');
}

拖放API实现

HTML5原生拖放功能:

const draggable = document.getElementById('draggable');
const dropzone = document.getElementById('dropzone');

draggable.addEventListener('dragstart', (e) => {
  e.dataTransfer.setData('text/plain', draggable.id);
  draggable.classList.add('dragging');
});

dropzone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropzone.classList.add('drag-over');
});

dropzone.addEventListener('drop', (e) => {
  e.preventDefault();
  const id = e.dataTransfer.getData('text/plain');
  const element = document.getElementById(id);
  dropzone.appendChild(element);
  dropzone.classList.remove('drag-over');
});

页面生命周期事件

监听页面状态变化:

// 页面可见性变化
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    console.log('页面被隐藏');
  } else {
    console.log('页面可见');
  }
});

// 页面加载状态
window.addEventListener('load', () => {
  console.log('所有资源加载完成');
});

window.addEventListener('DOMContentLoaded', () => {
  console.log('DOM解析完成');
});

// 页面卸载前
window.addEventListener('beforeunload', (e) => {
  e.preventDefault();
  e.returnValue = '确定离开吗?';
});

网络状态事件

检测网络连接变化:

window.addEventListener('online', () => {
  console.log('网络连接恢复');
  showNotification('您已恢复网络连接');
});

window.addEventListener('offline', () => {
  console.log('网络连接断开');
  showNotification('您已离线,部分功能不可用');
});

// 使用navigator检测当前状态
if (!navigator.onLine) {
  console.log('当前处于离线状态');
}

媒体查询事件

响应式设计中的媒体查询监听:

const mediaQuery = window.matchMedia('(max-width: 600px)');

function handleTabletChange(e) {
  if (e.matches) {
    console.log('切换到移动布局');
    activateMobileLayout();
  } else {
    console.log('切换到桌面布局');
    activateDesktopLayout();
  }
}

// 初始检查
handleTabletChange(mediaQuery);

// 添加监听
mediaQuery.addListener(handleTabletChange);

Web Workers通信

与Worker线程的事件通信:

// 主线程
const worker = new Worker('worker.js');

worker.addEventListener('message', (event) => {
  console.log('收到Worker消息:', event.data);
  document.getElementById('result').textContent = event.data;
});

document.getElementById('start').addEventListener('click', () => {
  worker.postMessage('开始计算');
});

// worker.js
self.addEventListener('message', (e) => {
  if (e.data === '开始计算') {
    const result = heavyComputation();
    self.postMessage(result);
  }
});

WebSocket事件处理

实时通信的事件模型:

const socket = new WebSocket('wss://example.com/socket');

socket.addEventListener('open', () => {
  console.log('连接已建立');
  socket.send('Hello Server!');
});

socket.addEventListener('message', (event) => {
  console.log('收到消息:', event.data);
  updateUI(JSON.parse(event.data));
});

socket.addEventListener('close', () => {
  console.log('连接已关闭');
  attemptReconnect();
});

socket.addEventListener('error', (error) => {
  console.error('WebSocket错误:', error);
});

事件监听的内存管理

避免内存泄漏的关键实践:

// 不好的做法:匿名函数无法移除
element.addEventListener('click', () => {
  console.log('点击');
});

// 正确做法:使用具名函数
function handleClick() {
  console.log('点击');
}

element.addEventListener('click', handleClick);

// 需要时移除
element.removeEventListener('click', handleClick);

// 批量移除示例
const events = [
  { element: button1, type: 'click', handler: handleClick },
  { element: button2, type: 'mouseover', handler: handleHover }
];

function cleanupEvents() {
  events.forEach(({ element, type, handler }) => {
    element.removeEventListener(type, handler);
  });
}

被动事件监听器

提升滚动性能的优化技巧:

// 传统方式可能阻塞滚动
window.addEventListener('scroll', () => {
  console.log('滚动位置:', window.scrollY);
});

// 使用passive改进
window.addEventListener('scroll', () => {
  console.log('被动事件监听');
}, { passive: true });

// 检测passive支持
let passiveSupported = false;
try {
  const options = {
    get passive() {
      passiveSupported = true;
      return false;
    }
  };
  window.addEventListener('test', null, options);
  window.removeEventListener('test', null, options);
} catch (err) {
  passiveSupported = false;
}

事件监听的测试策略

单元测试中的事件模拟:

// 使用Jest测试事件处理
describe('点击事件测试', () => {
  it('应该处理点击事件', () => {
    const mockHandler = jest.fn();
    const button = document.createElement('button');
    button.addEventListener('click', mockHandler);
    
    // 模拟点击
    button.dispatchEvent(new MouseEvent('click', {
      bubbles: true,
      cancelable: true
    }));
    
    expect(mockHandler).toHaveBeenCalledTimes(1);
  });
});

// 测试键盘事件
it('应该响应回车键', () => {
  const input = document.createElement('input');
  const mockHandler = jest.fn();
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') mockHandler();
  });
  
  input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
  expect(mockHandler).toHaveBeenCalled();
});

事件系统的底层原理

了解事件机制的内部实现:

// 简单的事件系统实现
class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(type, listener) {
    this.events[type] = this.events[type] || [];
    this.events[type].push(listener);
  }

  emit(type, ...args) {
    if (this.events[type]) {
      this.events[type].forEach(listener => listener(...args));
    }
  }

  off(type, listener) {
    if (this.events[type]) {
      this.events[type] = this.events[type].filter(l => l !== listener);
    }
  }
}

// 使用示例
const emitter = new EventEmitter();
emitter.on('data', (data) => console.log('收到数据:', data));
emitter.emit('data', { id: 1, value: 'test' });

性能监控事件

使用Performance API监听关键事件:

// 监听长任务
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('长任务:', entry);
    if (entry.duration > 50) {
      reportLongTask(entry);
    }
  }
});

observer.observe({ entryTypes: ['longtask'] });

// 监听布局变化
const layoutObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log('强制同步布局:', entry);
  });
});

layoutObserver.observe({ entryTypes: ['layout-shift'] });

错误处理事件

全局错误捕获机制:

// 全局错误处理
window.addEventListener('error', (event) => {
  console.error('全局错误:', event.error);
  sendErrorToServer(event);
  return true; // 阻止默认错误处理
});

// Promise拒绝捕获
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的Promise拒绝:', event.reason);
  event.preventDefault(); // 阻止默认控制台报错
});

// 资源加载失败
window.addEventListener('error', (event) => {
  if (event.target instanceof HTMLElement) {
    console.log('资源加载失败:', event.target.src || event.target.href);
    replaceBrokenImage(event.target);
  }
}, true); // 使用捕获阶段

剪贴板事件处理

安全地访问剪贴板内容:

document.addEventListener('copy', (event) => {
  const selection = window.getSelection();
  event.clipboardData.setData('text/plain', `来源: 我的网站\n${selection}`);
  event.preventDefault(); // 阻止默认复制行为
});

document.addEventListener('paste', async (event) => {
  const items = event.clipboardData.items;
  for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf('image') !== -1) {
      const blob = items[i].getAsFile();
      const imageUrl = URL.createObjectURL(blob);
      displayPastedImage(imageUrl);
      break;
    }
  }
});

// 现代异步剪贴板API
document.getElementById('pasteBtn').addEventListener('click', async () => {
  try {
    const text = await navigator.clipboard.readText();
    console.log('剪贴板文本:', text);
  } catch (err) {
    console.error('剪贴板访问失败:', err);
  }
});

全屏变化事件

检测全屏状态变化:

document.addEventListener('fullscreenchange', () => {
  if (document.fullscreenElement) {
    console.log('进入全屏模式');
    adjustUIForFullscreen();
  } else {
    console.log('退出全屏模式');
    restoreUI();
  }
});

// 跨浏览器兼容
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('MSFullscreenChange', handleFullscreenChange);

// 请求全屏
document.getElementById('fullscreenBtn').addEventListener('click', () => {
  const elem = document.documentElement;
  if (elem.requestFullscreen) {
    elem.requestFullscreen();
  } else if (elem.webkitRequestFullscreen) {
    elem.webkitRequestFullscreen();
  } else if (elem.mozRequestFullScreen) {
    elem.mozRequestFullScreen();
  } else if (elem.msRequestFullscreen) {
    elem.msRequestFullscreen();
  }
});

设备方向事件

处理移动设备传感器数据:

// 陀螺仪和加速度计
window.addEventListener('deviceorientation', (event) => {
  console.log('设备方向:', 
    `alpha: ${event.alpha}`, 
    `beta: ${event.beta}`,
    `gamma: ${event.gamma}`
  );
  updateCompass(event.alpha);
});

window.addEventListener('devicemotion', (event) => {
  const acceleration = event.accelerationIncludingGravity;
  console.log('设备加速度:', 
    `X: ${acceleration.x}`,
    `Y: ${acceleration.y}`,
    `Z: ${acceleration.z}`
  );
  detectShake(acceleration);
});

// 需要先请求权限
function requestMotionPermission() {
  if (typeof DeviceOrientationEvent !== 'undefined' && 
      typeof DeviceOrientationEvent.requestPermission === 'function') {
    DeviceOrientationEvent.requestPermission()
      .then(response => {
        if (response === 'granted') {
          window.addEventListener('deviceorientation', handleOrientation);
        }
      })
      .catch(console.error);
  }
}

页面可见性API

优化后台标签页的资源使用:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // 页面不可见时
    pauseAnimations();
    stopVideoPlayback();
    throttleNetworkRequests();
  } else {
    // 页面恢复可见
    resumeAnimations();
    startVideoPlayback();
    normalNetworkRequests();
  }
});

// 检测当前状态
function logVisibility() {
  console.log('页面可见性状态:',
    document.visibilityState,
    `隐藏: ${document.hidden}`
  );
}

// 定时器优化
let timer;
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    clearInterval(timer);
  } else {
    timer = setInterval(updateDashboard, 5000);
  }
});

浏览器历史事件

监听导航行为:

// 监听路由变化
window.addEventListener('popstate', (event) => {
  console.log('位置变化:', event.state);
  loadContentForRoute(location.pathname);
});

// 监听hash变化
window.addEventListener('hashchange', () => {
  console.log('hash变化:', location.hash);
  highlightCurrentSection();

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:异步流程控制

下一篇:navigator对象属性

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌