轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Node.js

  • Vue.js

  • 工程化

  • 浏览器与Web API

    • HTML基础-语义化标签与文档结构
    • HTML基础-文本与排版标签
    • HTML基础-列表与表格
    • HTML表单-Input类型详解
    • HTML表单-表单元素与验证
    • HTML交互-多媒体元素
    • HTML工程化-模板与组件化
    • HTML工程化-性能优化
    • CSS基础-选择器与优先级
    • CSS基础-盒模型与布局基础
    • CSS基础-单位与颜色系统
    • CSS基础-文本与字体
    • CSS基础-背景、列表与表格样式
    • CSS布局-Flexbox完全指南
    • CSS布局-Grid网格布局
    • CSS布局-响应式设计实践
    • CSS进阶-动画与过渡
    • CSS进阶-渐变与阴影效果
    • CSS进阶-Transform与3D变换
    • CSS进阶-滤镜与混合模式
    • 现代CSS-CSS预处理器对比
    • 现代CSS-CSS-in-JS方案
    • 现代CSS-原子化CSS与Tailwind
    • CSS工程化-架构与规范
    • CSS工程化-性能优化
    • CSS工程化-PostCSS实战指南
    • Web API基础-DOM操作完全指南
    • Web API基础-事件处理与委托
      • 一、事件基础
        • 1.1 什么是事件
        • 1.2 添加事件监听器
        • 1.3 移除事件监听器
        • 1.4 addEventListener选项
      • 二、事件对象
        • 2.1 Event对象属性
        • 2.2 鼠标事件对象
        • 2.3 键盘事件对象
        • 2.4 表单事件对象
      • 三、事件传播
        • 3.1 事件流三个阶段
        • 3.2 阻止事件传播
        • 3.3 阻止默认行为
      • 四、事件委托
        • 4.1 什么是事件委托
        • 4.2 基础事件委托
        • 4.3 处理动态元素
        • 4.4 高级事件委托
        • 4.5 事件委托工具函数
      • 五、常用事件类型
        • 5.1 鼠标事件
        • 5.2 键盘事件
        • 5.3 表单事件
        • 5.4 文档和窗口事件
        • 5.5 拖放事件
      • 六、自定义事件
        • 6.1 创建和触发自定义事件
        • 6.2 组件间通信
        • 6.3 Event vs CustomEvent
      • 七、综合示例
      • 八、性能优化
        • 8.1 事件节流(Throttle)
        • 8.2 事件防抖(Debounce)
        • 8.3 passive事件监听器
        • 8.4 移除不需要的监听器
      • 九、最佳实践
        • 9.1 事件监听
        • 9.2 事件处理
        • 9.3 事件委托
      • 十、总结
    • Web API基础-BOM与浏览器环境
    • Web API存储-客户端存储方案
    • Web API网络-HTTP请求详解
    • Web API网络-实时通信方案
    • Web API交互-用户体验增强
    • HTML&CSS历代版本新特性
  • 前端
  • 浏览器与Web API
轩辕李
2019-08-12
目录

Web API基础-事件处理与委托

# Web API基础-事件处理与委托

事件是JavaScript与用户交互的核心机制。通过事件,我们可以响应用户的点击、输入、滚动等操作。本文将深入讲解事件处理的各个方面,包括事件监听、事件对象、事件传播、事件委托等内容。

# 一、事件基础

# 1.1 什么是事件

事件是用户或浏览器执行的某种动作,例如:

  • 用户交互:click、dblclick、mousedown、keypress、scroll
  • 页面生命周期:DOMContentLoaded、load、beforeunload
  • 表单相关:submit、input、change、focus、blur
  • 媒体相关:play、pause、ended

# 1.2 添加事件监听器

// 方式1:HTML属性(不推荐)
<button onclick="handleClick()">点击</button>

// 方式2:DOM属性(会覆盖)
const btn = document.querySelector('button');
btn.onclick = function() {
  console.log('点击了');
};

// 方式3:addEventListener(推荐)
btn.addEventListener('click', function(e) {
  console.log('点击了', e);
});

// 可以添加多个监听器
btn.addEventListener('click', handler1);
btn.addEventListener('click', handler2); // 不会覆盖handler1

# 1.3 移除事件监听器

function handleClick(e) {
  console.log('点击了');
}

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

// 添加监听器
btn.addEventListener('click', handleClick);

// 移除监听器(必须是同一个函数引用)
btn.removeEventListener('click', handleClick);

// ✗ 无法移除:匿名函数
btn.addEventListener('click', function() {
  console.log('点击');
});
btn.removeEventListener('click', function() {
  console.log('点击');
}); // 无效,不是同一个函数引用

# 1.4 addEventListener选项

element.addEventListener('click', handler, {
  capture: false,  // 是否在捕获阶段触发
  once: true,      // 只触发一次后自动移除
  passive: true,   // 不会调用preventDefault()
  signal: abortController.signal // 用于批量移除
});

// 简写形式
element.addEventListener('click', handler, true); // capture: true

// 使用AbortController批量移除
const controller = new AbortController();

btn1.addEventListener('click', handler1, { signal: controller.signal });
btn2.addEventListener('click', handler2, { signal: controller.signal });
btn3.addEventListener('click', handler3, { signal: controller.signal });

// 一次性移除所有
controller.abort();

# 二、事件对象

# 2.1 Event对象属性

element.addEventListener('click', function(e) {
  // 事件类型
  console.log(e.type); // "click"
  
  // 事件目标
  console.log(e.target);       // 实际触发事件的元素
  console.log(e.currentTarget); // 绑定事件的元素
  
  // 时间戳
  console.log(e.timeStamp); // 事件发生的时间
  
  // 事件阶段
  console.log(e.eventPhase);
  // 1: 捕获阶段
  // 2: 目标阶段
  // 3: 冒泡阶段
  
  // 是否可冒泡
  console.log(e.bubbles); // true/false
  
  // 是否可取消
  console.log(e.cancelable); // true/false
});

# 2.2 鼠标事件对象

element.addEventListener('click', function(e) {
  // 鼠标位置(相对于视口)
  console.log(e.clientX, e.clientY);
  
  // 鼠标位置(相对于页面)
  console.log(e.pageX, e.pageY);
  
  // 鼠标位置(相对于屏幕)
  console.log(e.screenX, e.screenY);
  
  // 鼠标位置(相对于目标元素)
  console.log(e.offsetX, e.offsetY);
  
  // 按下的鼠标按钮
  console.log(e.button);
  // 0: 左键
  // 1: 中键
  // 2: 右键
  
  // 修饰键
  console.log(e.ctrlKey);  // 是否按下Ctrl
  console.log(e.shiftKey); // 是否按下Shift
  console.log(e.altKey);   // 是否按下Alt
  console.log(e.metaKey);  // 是否按下Meta(Mac的Cmd)
  
  // 相关元素(mouseover/mouseout)
  console.log(e.relatedTarget);
});

# 2.3 键盘事件对象

element.addEventListener('keydown', function(e) {
  // 按键代码
  console.log(e.key);      // 按键字符,如 "a", "Enter", "ArrowUp"
  console.log(e.code);     // 物理按键,如 "KeyA", "Enter", "ArrowUp"
  console.log(e.keyCode);  // 已废弃,不推荐使用
  
  // 修饰键
  console.log(e.ctrlKey);
  console.log(e.shiftKey);
  console.log(e.altKey);
  console.log(e.metaKey);
  
  // 是否重复触发(长按)
  console.log(e.repeat);
  
  // 组合键判断
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();
    console.log('Ctrl+S 保存');
  }
});

# 2.4 表单事件对象

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

input.addEventListener('input', function(e) {
  // 输入值
  console.log(e.target.value);
  
  // inputType(详细输入类型)
  console.log(e.inputType);
  // "insertText", "deleteContentBackward" 等
});

input.addEventListener('change', function(e) {
  console.log('值改变了:', e.target.value);
});

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

form.addEventListener('submit', function(e) {
  e.preventDefault(); // 阻止表单提交
  
  const formData = new FormData(e.target);
  console.log(Object.fromEntries(formData));
});

# 三、事件传播

# 3.1 事件流三个阶段

事件传播分为三个阶段:

1. 捕获阶段(Capturing):从window到目标元素
2. 目标阶段(Target):到达目标元素
3. 冒泡阶段(Bubbling):从目标元素返回window
<div id="outer">
  <div id="middle">
    <div id="inner">点击我</div>
  </div>
</div>

<script>
  const outer = document.getElementById('outer');
  const middle = document.getElementById('middle');
  const inner = document.getElementById('inner');
  
  // 冒泡阶段(默认)
  outer.addEventListener('click', () => console.log('outer 冒泡'));
  middle.addEventListener('click', () => console.log('middle 冒泡'));
  inner.addEventListener('click', () => console.log('inner 冒泡'));
  
  // 捕获阶段
  outer.addEventListener('click', () => console.log('outer 捕获'), true);
  middle.addEventListener('click', () => console.log('middle 捕获'), true);
  inner.addEventListener('click', () => console.log('inner 捕获'), true);
  
  // 点击inner,输出顺序:
  // outer 捕获
  // middle 捕获
  // inner 捕获
  // inner 冒泡
  // middle 冒泡
  // outer 冒泡
</script>

# 3.2 阻止事件传播

element.addEventListener('click', function(e) {
  // 阻止事件继续传播(冒泡或捕获)
  e.stopPropagation();
  
  // 立即阻止事件传播(同一元素的其他监听器也不会执行)
  e.stopImmediatePropagation();
});
<div id="parent">
  <button id="child">点击</button>
</div>

<script>
  const parent = document.getElementById('parent');
  const child = document.getElementById('child');
  
  parent.addEventListener('click', () => {
    console.log('父元素点击');
  });
  
  child.addEventListener('click', (e) => {
    console.log('子元素点击');
    e.stopPropagation(); // 阻止冒泡到父元素
  });
  
  // 点击button,只输出:子元素点击
</script>

# 3.3 阻止默认行为

// 阻止链接跳转
document.querySelector('a').addEventListener('click', function(e) {
  e.preventDefault();
  console.log('链接被阻止');
});

// 阻止表单提交
document.querySelector('form').addEventListener('submit', function(e) {
  e.preventDefault();
  console.log('表单提交被阻止');
});

// 阻止右键菜单
document.addEventListener('contextmenu', function(e) {
  e.preventDefault();
});

// 检查是否可以阻止
if (e.cancelable) {
  e.preventDefault();
}

# 四、事件委托

# 4.1 什么是事件委托

事件委托利用事件冒泡机制,将事件监听器添加到父元素上,通过判断e.target来处理子元素的事件。

优势:

  1. 减少内存占用(只需一个监听器)
  2. 动态元素自动绑定事件
  3. 提升性能

# 4.2 基础事件委托

<ul id="list">
  <li>项目1</li>
  <li>项目2</li>
  <li>项目3</li>
</ul>

<script>
  // ✗ 不推荐:为每个li添加监听器
  const items = document.querySelectorAll('li');
  items.forEach(item => {
    item.addEventListener('click', function() {
      console.log(this.textContent);
    });
  });
  
  // ✓ 推荐:事件委托
  const list = document.getElementById('list');
  list.addEventListener('click', function(e) {
    if (e.target.tagName === 'LI') {
      console.log(e.target.textContent);
    }
  });
</script>

# 4.3 处理动态元素

const list = document.getElementById('list');

// 事件委托
list.addEventListener('click', function(e) {
  if (e.target.tagName === 'LI') {
    console.log('点击了:', e.target.textContent);
  }
});

// 动态添加元素
const newItem = document.createElement('li');
newItem.textContent = '新项目';
list.appendChild(newItem); // 自动具有点击事件

# 4.4 高级事件委托

<div id="container">
  <button class="action" data-action="edit">编辑</button>
  <button class="action" data-action="delete">删除</button>
  <button class="action" data-action="share">分享</button>
</div>

<script>
  const container = document.getElementById('container');
  
  container.addEventListener('click', function(e) {
    // 使用closest查找最近的匹配元素
    const actionBtn = e.target.closest('.action');
    
    if (!actionBtn) return;
    
    const action = actionBtn.dataset.action;
    
    switch(action) {
      case 'edit':
        console.log('编辑操作');
        break;
      case 'delete':
        console.log('删除操作');
        break;
      case 'share':
        console.log('分享操作');
        break;
    }
  });
</script>

# 4.5 事件委托工具函数

function delegate(parent, selector, eventType, handler) {
  parent.addEventListener(eventType, function(e) {
    const target = e.target.closest(selector);
    
    if (target && parent.contains(target)) {
      handler.call(target, e);
    }
  });
}

// 使用示例
delegate(document.body, '.delete-btn', 'click', function(e) {
  console.log('删除按钮被点击', this);
});

delegate(document.body, '.item', 'mouseenter', function(e) {
  this.classList.add('hover');
});

# 五、常用事件类型

# 5.1 鼠标事件

// 点击事件
element.addEventListener('click', handler);        // 单击
element.addEventListener('dblclick', handler);     // 双击
element.addEventListener('contextmenu', handler);  // 右键菜单

// 鼠标按下和释放
element.addEventListener('mousedown', handler);    // 按下
element.addEventListener('mouseup', handler);      // 释放

// 鼠标移动
element.addEventListener('mousemove', handler);    // 移动
element.addEventListener('mouseenter', handler);   // 进入(不冒泡)
element.addEventListener('mouseleave', handler);   // 离开(不冒泡)
element.addEventListener('mouseover', handler);    // 进入(冒泡)
element.addEventListener('mouseout', handler);     // 离开(冒泡)

// 滚轮事件
element.addEventListener('wheel', handler);

mouseenter/mouseleave vs mouseover/mouseout:

<div id="parent">
  <div id="child">子元素</div>
</div>

<script>
  const parent = document.getElementById('parent');
  
  // mouseenter/mouseleave:不会因为子元素触发
  parent.addEventListener('mouseenter', () => {
    console.log('进入父元素'); // 只触发一次
  });
  
  // mouseover/mouseout:会因为子元素触发
  parent.addEventListener('mouseover', () => {
    console.log('over父元素'); // 进入子元素时也会触发
  });
</script>

# 5.2 键盘事件

// 键盘事件顺序:keydown -> keypress -> keyup
element.addEventListener('keydown', handler);   // 按键按下
element.addEventListener('keypress', handler);  // 产生字符(已废弃)
element.addEventListener('keyup', handler);     // 按键释放

// 实际应用
document.addEventListener('keydown', function(e) {
  // Esc关闭弹窗
  if (e.key === 'Escape') {
    closeModal();
  }
  
  // Ctrl+S保存
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();
    save();
  }
  
  // 方向键导航
  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
    navigate(e.key);
  }
});

# 5.3 表单事件

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

// 输入事件
input.addEventListener('input', function(e) {
  console.log('实时输入:', e.target.value);
});

// 值改变事件
input.addEventListener('change', function(e) {
  console.log('值改变:', e.target.value);
});

// 焦点事件
input.addEventListener('focus', handler);   // 获得焦点
input.addEventListener('blur', handler);    // 失去焦点
input.addEventListener('focusin', handler); // 获得焦点(冒泡)
input.addEventListener('focusout', handler);// 失去焦点(冒泡)

// 表单提交
form.addEventListener('submit', function(e) {
  e.preventDefault();
  // 表单验证和提交逻辑
});

// 表单重置
form.addEventListener('reset', function(e) {
  if (!confirm('确定重置表单?')) {
    e.preventDefault();
  }
});

# 5.4 文档和窗口事件

// 文档加载
document.addEventListener('DOMContentLoaded', function() {
  console.log('DOM加载完成');
});

window.addEventListener('load', function() {
  console.log('页面完全加载(包括图片等资源)');
});

// 页面离开
window.addEventListener('beforeunload', function(e) {
  e.preventDefault();
  e.returnValue = ''; // 显示确认对话框
});

window.addEventListener('unload', function() {
  // 页面卸载时执行清理操作
});

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

// 滚动事件
window.addEventListener('scroll', function() {
  console.log('滚动位置:', window.scrollY);
});

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

# 5.5 拖放事件

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

// 拖动元素
draggable.addEventListener('dragstart', function(e) {
  e.dataTransfer.setData('text/plain', e.target.id);
  e.dataTransfer.effectAllowed = 'move';
});

draggable.addEventListener('drag', handler);
draggable.addEventListener('dragend', handler);

// 放置区域
dropzone.addEventListener('dragenter', handler);
dropzone.addEventListener('dragover', function(e) {
  e.preventDefault(); // 必须阻止默认行为才能drop
});

dropzone.addEventListener('dragleave', handler);

dropzone.addEventListener('drop', function(e) {
  e.preventDefault();
  const id = e.dataTransfer.getData('text/plain');
  const element = document.getElementById(id);
  dropzone.appendChild(element);
});

# 六、自定义事件

# 6.1 创建和触发自定义事件

// 创建自定义事件
const event = new CustomEvent('myEvent', {
  detail: { message: '自定义数据' },
  bubbles: true,
  cancelable: true
});

// 监听自定义事件
element.addEventListener('myEvent', function(e) {
  console.log('自定义事件触发:', e.detail.message);
});

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

# 6.2 组件间通信

// 组件A:发送事件
class ComponentA {
  notify() {
    const event = new CustomEvent('data-updated', {
      detail: { data: this.data },
      bubbles: true
    });
    
    this.element.dispatchEvent(event);
  }
}

// 组件B:监听事件
class ComponentB {
  constructor() {
    document.addEventListener('data-updated', (e) => {
      console.log('收到数据:', e.detail.data);
      this.update(e.detail.data);
    });
  }
}

# 6.3 Event vs CustomEvent

// Event(基础事件)
const event1 = new Event('click', {
  bubbles: true,
  cancelable: true
});

// CustomEvent(可携带自定义数据)
const event2 = new CustomEvent('myEvent', {
  detail: { custom: 'data' },
  bubbles: true,
  cancelable: true
});

# 七、综合示例

<html>
  <div id="event-demo-w5x6y">
    <h3>事件处理综合示例</h3>
    
    <div class="tabs-w5x6y">
      <button class="tab-btn-w5x6y active-w5x6y" data-tab="delegation">事件委托</button>
      <button class="tab-btn-w5x6y" data-tab="keyboard">键盘事件</button>
      <button class="tab-btn-w5x6y" data-tab="drag">拖放事件</button>
    </div>
    
    <div class="tab-content-w5x6y">
      <div id="delegation-w5x6y" class="panel-w5x6y active-w5x6y">
        <h4>事件委托示例</h4>
        <div class="controls-w5x6y">
          <input type="text" id="item-input-w5x6y" placeholder="输入项目名称">
          <button id="add-item-w5x6y">添加项目</button>
        </div>
        <ul id="item-list-w5x6y" class="item-list-w5x6y"></ul>
      </div>
      
      <div id="keyboard-w5x6y" class="panel-w5x6y">
        <h4>键盘事件示例</h4>
        <p>在输入框中按下键盘查看事件信息</p>
        <input type="text" id="key-input-w5x6y" class="key-input-w5x6y" placeholder="在此输入...">
        <div class="key-info-w5x6y">
          <div><strong>按键:</strong> <span id="key-w5x6y">-</span></div>
          <div><strong>Code:</strong> <span id="code-w5x6y">-</span></div>
          <div><strong>修饰键:</strong> <span id="modifiers-w5x6y">-</span></div>
        </div>
      </div>
      
      <div id="drag-w5x6y" class="panel-w5x6y">
        <h4>拖放事件示例</h4>
        <div class="drag-container-w5x6y">
          <div class="drag-zone-w5x6y" id="source-w5x6y">
            <div class="drag-item-w5x6y" draggable="true" data-id="1">项目 1</div>
            <div class="drag-item-w5x6y" draggable="true" data-id="2">项目 2</div>
            <div class="drag-item-w5x6y" draggable="true" data-id="3">项目 3</div>
          </div>
          <div class="drag-zone-w5x6y" id="target-w5x6y">
            <p class="placeholder-w5x6y">拖放到这里</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</html>

<style>
  #event-demo-w5x6y {
    font-family: sans-serif;
    max-width: 700px;
  }
  
  .tabs-w5x6y {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
    border-bottom: 2px solid #e0e0e0;
  }
  
  .tab-btn-w5x6y {
    padding: 10px 20px;
    border: none;
    background: transparent;
    cursor: pointer;
    font-size: 14px;
    color: #666;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    transition: all 0.2s;
  }
  
  .tab-btn-w5x6y:hover {
    color: #2196F3;
  }
  
  .tab-btn-w5x6y.active-w5x6y {
    color: #2196F3;
    border-bottom-color: #2196F3;
  }
  
  .panel-w5x6y {
    display: none;
    padding: 20px;
    background: #f9f9f9;
    border-radius: 8px;
  }
  
  .panel-w5x6y.active-w5x6y {
    display: block;
  }
  
  .controls-w5x6y {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
  }
  
  #item-input-w5x6y {
    flex: 1;
    padding: 8px 12px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
  }
  
  #add-item-w5x6y {
    padding: 8px 16px;
    background: #2196F3;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
  }
  
  .item-list-w5x6y {
    list-style: none;
    padding: 0;
    margin: 0;
  }
  
  .list-item-w5x6y {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px;
    background: white;
    border-radius: 4px;
    margin-bottom: 8px;
    border: 1px solid #e0e0e0;
  }
  
  .item-delete-w5x6y {
    padding: 4px 12px;
    background: #f44336;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
  }
  
  .key-input-w5x6y {
    width: 100%;
    padding: 12px;
    border: 2px solid #2196F3;
    border-radius: 4px;
    font-size: 16px;
    margin-bottom: 16px;
  }
  
  .key-info-w5x6y {
    display: grid;
    gap: 12px;
    padding: 16px;
    background: white;
    border-radius: 4px;
    border: 1px solid #e0e0e0;
  }
  
  .key-info-w5x6y div {
    display: flex;
    gap: 8px;
  }
  
  .key-info-w5x6y strong {
    min-width: 80px;
  }
  
  .key-info-w5x6y span {
    color: #2196F3;
    font-family: monospace;
  }
  
  .drag-container-w5x6y {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
  }
  
  .drag-zone-w5x6y {
    min-height: 200px;
    padding: 16px;
    background: white;
    border: 2px dashed #ddd;
    border-radius: 8px;
  }
  
  .drag-zone-w5x6y.drag-over-w5x6y {
    border-color: #2196F3;
    background: #e3f2fd;
  }
  
  .drag-item-w5x6y {
    padding: 12px;
    background: #2196F3;
    color: white;
    border-radius: 4px;
    margin-bottom: 8px;
    cursor: move;
    user-select: none;
  }
  
  .drag-item-w5x6y.dragging-w5x6y {
    opacity: 0.5;
  }
  
  .placeholder-w5x6y {
    text-align: center;
    color: #999;
    margin: 80px 0;
  }
</style>

<script>
  const tabs = document.querySelectorAll('.tab-btn-w5x6y');
  const panels = document.querySelectorAll('.panel-w5x6y');
  
  document.querySelector('.tabs-w5x6y').addEventListener('click', (e) => {
    const btn = e.target.closest('.tab-btn-w5x6y');
    if (!btn) return;
    
    const targetTab = btn.dataset.tab;
    
    tabs.forEach(t => t.classList.remove('active-w5x6y'));
    panels.forEach(p => p.classList.remove('active-w5x6y'));
    
    btn.classList.add('active-w5x6y');
    document.getElementById(`${targetTab}-w5x6y`).classList.add('active-w5x6y');
  });

  const itemInput = document.getElementById('item-input-w5x6y');
  const addBtn = document.getElementById('add-item-w5x6y');
  const itemList = document.getElementById('item-list-w5x6y');
  
  function addItem() {
    const text = itemInput.value.trim();
    if (!text) return;
    
    const li = document.createElement('li');
    li.className = 'list-item-w5x6y';
    li.innerHTML = `
      <span>${text}</span>
      <button class="item-delete-w5x6y">删除</button>
    `;
    
    itemList.appendChild(li);
    itemInput.value = '';
  }
  
  addBtn.addEventListener('click', addItem);
  
  itemInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') {
      addItem();
    }
  });
  
  itemList.addEventListener('click', (e) => {
    if (e.target.classList.contains('item-delete-w5x6y')) {
      e.target.closest('.list-item-w5x6y').remove();
    }
  });

  const keyInput = document.getElementById('key-input-w5x6y');
  const keyDisplay = document.getElementById('key-w5x6y');
  const codeDisplay = document.getElementById('code-w5x6y');
  const modifiersDisplay = document.getElementById('modifiers-w5x6y');
  
  keyInput.addEventListener('keydown', (e) => {
    keyDisplay.textContent = e.key;
    codeDisplay.textContent = e.code;
    
    const modifiers = [];
    if (e.ctrlKey) modifiers.push('Ctrl');
    if (e.shiftKey) modifiers.push('Shift');
    if (e.altKey) modifiers.push('Alt');
    if (e.metaKey) modifiers.push('Meta');
    
    modifiersDisplay.textContent = modifiers.length > 0 ? modifiers.join(' + ') : '无';
  });

  const sourceZone = document.getElementById('source-w5x6y');
  const targetZone = document.getElementById('target-w5x6y');
  
  let draggedElement = null;
  
  document.addEventListener('dragstart', (e) => {
    if (e.target.classList.contains('drag-item-w5x6y')) {
      draggedElement = e.target;
      e.target.classList.add('dragging-w5x6y');
      e.dataTransfer.effectAllowed = 'move';
    }
  });
  
  document.addEventListener('dragend', (e) => {
    if (e.target.classList.contains('drag-item-w5x6y')) {
      e.target.classList.remove('dragging-w5x6y');
    }
  });
  
  [sourceZone, targetZone].forEach(zone => {
    zone.addEventListener('dragover', (e) => {
      e.preventDefault();
      zone.classList.add('drag-over-w5x6y');
    });
    
    zone.addEventListener('dragleave', (e) => {
      if (e.target === zone) {
        zone.classList.remove('drag-over-w5x6y');
      }
    });
    
    zone.addEventListener('drop', (e) => {
      e.preventDefault();
      zone.classList.remove('drag-over-w5x6y');
      
      if (draggedElement) {
        const placeholder = zone.querySelector('.placeholder-w5x6y');
        if (placeholder) {
          placeholder.remove();
        }
        
        zone.appendChild(draggedElement);
        draggedElement = null;
      }
    });
  });
</script>

# 八、性能优化

# 8.1 事件节流(Throttle)

function throttle(func, delay) {
  let lastCall = 0;
  
  return function(...args) {
    const now = Date.now();
    
    if (now - lastCall >= delay) {
      lastCall = now;
      func.apply(this, args);
    }
  };
}

// 使用示例
window.addEventListener('scroll', throttle(function() {
  console.log('滚动事件');
}, 200)); // 每200ms最多执行一次

# 8.2 事件防抖(Debounce)

function debounce(func, delay) {
  let timeoutId;
  
  return function(...args) {
    clearTimeout(timeoutId);
    
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 使用示例
const input = document.querySelector('input');
input.addEventListener('input', debounce(function(e) {
  console.log('搜索:', e.target.value);
}, 300)); // 停止输入300ms后执行

# 8.3 passive事件监听器

// 提升滚动性能
document.addEventListener('touchstart', handler, { passive: true });
document.addEventListener('wheel', handler, { passive: true });

// passive: true 表示不会调用preventDefault()
// 浏览器可以立即开始滚动,不需要等待JavaScript执行

# 8.4 移除不需要的监听器

class Component {
  constructor(element) {
    this.element = element;
    this.handleClick = this.handleClick.bind(this);
    this.element.addEventListener('click', this.handleClick);
  }
  
  handleClick(e) {
    console.log('点击');
  }
  
  destroy() {
    // 组件销毁时移除监听器
    this.element.removeEventListener('click', this.handleClick);
  }
}

# 九、最佳实践

# 9.1 事件监听

  1. 优先使用addEventListener而不是onclick
  2. 使用事件委托减少监听器数量
  3. 及时移除不需要的监听器
  4. 使用passive优化滚动性能
  5. 使用节流/防抖优化高频事件

# 9.2 事件处理

  1. 合理使用preventDefault()和stopPropagation()
  2. 避免在事件处理器中执行耗时操作
  3. 使用requestAnimationFrame处理动画相关事件
  4. 注意this指向(使用箭头函数或bind)

# 9.3 事件委托

  1. 适用于大量相似元素
  2. 适用于动态添加的元素
  3. 使用closest()查找目标元素
  4. 检查元素是否在容器内

# 十、总结

事件处理是JavaScript交互的核心:

  1. 事件监听:使用addEventListener添加事件监听器
  2. 事件对象:包含事件类型、目标、位置等信息
  3. 事件传播:理解捕获、目标、冒泡三个阶段
  4. 事件委托:利用冒泡机制优化性能
  5. 性能优化:节流、防抖、passive、及时移除监听器

掌握事件处理机制,能够构建高性能、交互丰富的Web应用。

祝你变得更强!

编辑 (opens new window)
#Web API#Event#JavaScript#事件委托
上次更新: 2025/11/28
Web API基础-DOM操作完全指南
Web API基础-BOM与浏览器环境

← Web API基础-DOM操作完全指南 Web API基础-BOM与浏览器环境→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code 最佳实践(个人版)
08-01
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式