第6章 6.2 事件处理与委托

🎯 开场:为什么你点的外卖总送错?

你有没有遇到过这种情况——在网上选了商品加入购物车,点了结算,结果页面毫无反应?或者一个按钮点下去,弹出了两个弹窗?

这种"我明明点了,系统不响应"或者"响应一次变两次"的bug,80% 都跟事件处理有关系。

上一章我们学了 querySelector 和基本的事件绑定(onclick = function()),但那种写法有个致命问题:每次动态添加新元素,还得重新绑一遍

举个例子——你有100个商品卡片,每个卡片里有个"加入购物车"按钮。用上一章的方法,你得循环100次,手动给每个按钮绑事件。但如果你后续又动态加载了20个新商品呢?新按钮又得重新绑...

这一章我们要解决的就是这个痛点。学完之后,你就能写出"加一个新元素进去,自动就有交互"的代码——这就是事件委托的核心威力。


🧱 基础:事件处理到底是怎么回事?

6.2.1 重新认识「事件」

先把一个认知扳过来。

事件不是「点一下就触发」这\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n么简单。 事件是一个完整的生命周期,像一场快递配送:

用户点击按钮
→ 事件开始从顶层往下传播(捕获阶段)
→ 到达目标元素(目标阶段)
→ 事件再从目标往上跑(冒泡阶段)
→ 整个过程结束

onclick 只在最后一个阶段生效,所以你"好像"感知不到前面的步骤。但事件委托的秘密,就藏在这些阶段里。

6.2.2 addEventListener:更专业的绑定方式

上一章的 onclick = function(){} 有个问题:同一个元素绑两次,后者会覆盖前者

const btn = document.querySelector('#btn')

btn.onclick = function() { console.log('第一次绑定') }
btn.onclick = function() { console.log('第二次绑定') }
// 点一下,只输出:第二次绑定

addEventListener 就不一样,它可以给同一个元素绑定多个处理函数

const btn = document.querySelector('#btn')

btn.addEventListener('click', function() {
console.log('第一次绑定')
})

btn.addEventListener('click', function() {
console.log('第二次绑定')
})

// 点一下,输出:
// 第一次绑定
// 第二次绑定

三句话解释:
- addEventListener('click', function(){}) 第一个参数是事件名(注意没有 on)
- 同一个事件可以绑定多个函数,按绑定顺序依次执行
- 不会覆盖!不会覆盖!不会覆盖!重要的事说三遍

6.2.3 事件对象:你的「快递单号」

每次事件触发,浏览器会自动往处理函数里塞一个参数,这个参数就是事件对象,包含了关于这次事件的所有信息。

const btn = document.querySelector('#btn')

btn.addEventListener('click', function(event) {
console.log('你点击了:', event.target)
console.log('事件类型:', event.type)
console.log('当前元素:', event.currentTarget)
})

点击按钮后,你会在控制台看到类似这样的输出:

你点击了: <button id="btn">按钮</button>
事件类型: click
当前元素: <button id="btn">按钮</button>

几个最常用的属性:
- event.target —— 实际被点击的元素
- event.currentTarget —— 当前绑定了事件处理器的元素
- event.preventDefault() —— 阻止默认行为(比如阻止链接跳转、表单提交)
- event.stopPropagation() —— 阻止事件继续冒泡

6.2.4 事件委托:让父元素替子元素干活

终于到重头戏了。

想象你是班主任,要管理全班50个学生的出勤情况。笨方法是逐个问"你来了吗?"——这就相当于给每个子元素单独绑定事件。

聪明方法是让班长帮你看着——全班只有一个班长,但ta能报告所有人的情况。这就是事件委托的思路。

核心原理: 事件会冒泡。如果给父元素绑定一个事件处理器,当子元素被点击时,事件会冒泡到父元素,父元素就能"感知"到。

// 不用委托:给每个 li 单独绑定
const items = document.querySelectorAll('li')
items.forEach(function(item) {
item.addEventListener('click', function() {
console.log('点击了:', item.textContent)
})
})

// 用委托:只给 ul 绑一次
const list = document.querySelector('ul')

list.addEventListener('click', function(event) {
// event.target 是实际被点击的元素
if (event.target.tagName === 'LI') {
console.log('点击了:', event.target.textContent)
}
})

两种写法效果一样,但委托的好处是:后面动态添加的 li,也自动拥有点击功能

6.2.5 事件捕获 vs 冒泡:快递的两条路线

前面说过,事件传播有两条路线:

element.addEventListener('click', handler, false)  // 冒泡阶段触发(默认)
element.addEventListener('click', handler, true)   // 捕获阶段触发

把事件传播想象成地铁:

  • 冒泡(默认):从车厢底部往上走,经过每一节车厢
  • 捕获:从车头往下走,先经过每一节再到底部

大部分场景用默认的冒泡就够了。但有些特殊情况(比如阻止别人拦截你的事件),需要用捕获。


🔥 实战:三个项目,从会用到真会用

项目 1:计数器——理解基础事件绑定

目标: 做一个点击按钮数字加1的计数器

<!DOCTYPE html>
<html>
<head>
<title>计数器</title>
</head>
<body>
<h1 id="count">0</h1>
<button id="addBtn">点我+1</button>
<button id="resetBtn">重置</button>

<script>
const countDisplay = document.getElementById('count')
const addBtn = document.getElementById('addBtn')
const resetBtn = document.getElementById('resetBtn')
let count = 0

// 给按钮绑定点击事件
addBtn.addEventListener('click', function() {
  count++
  countDisplay.textContent = count
  console.log('当前计数:', count)
})

resetBtn.addEventListener('click', function() {
  count = 0
  countDisplay.textContent = count
  console.log('已重置')
})
</script>
</body>
</html>

预期输出: 每次点击"点我+1",页面上的数字会 +1;点击"重置"数字变回 0

一句话解释:addEventListener 给两个按钮分别绑了点击处理函数,互不干扰。


项目 2:动态列表 + 事件委托——真实场景

目标: 实现一个待办清单,可以添加新事项,点击事项标记完成

<!DOCTYPE html>
<html>
<head>
<title>待办清单</title>
<style>
.completed { text-decoration: line-through; color: gray; }
</style>
</head>
<body>
<h1>我的待办</h1>
<input type="text" id="taskInput" placeholder="输入新任务...">
<button id="addTaskBtn">添加</button>
<ul id="taskList"></ul>

<script>
const taskInput = document.getElementById('taskInput')
const addTaskBtn = document.getElementById('addTaskBtn')
const taskList = document.getElementById('taskList')

// 添加任务
addTaskBtn.addEventListener('click', function() {
  const taskText = taskInput.value.trim()
  if (!taskText) {
    alert('请输入任务内容')
    return
  }

  // 创建新任务
  const newTask = document.createElement('li')
  newTask.textContent = taskText
  taskList.appendChild(newTask)

  taskInput.value = ''  // 清空输入框
  console.log('添加了新任务:', taskText)
})

// 事件委托:给 ul 绑定点击,整个列表都能响应
taskList.addEventListener('click', function(event) {
  // 只处理 li 的点击
  if (event.target.tagName === 'LI') {
    event.target.classList.toggle('completed')
    const isDone = event.target.classList.contains('completed')
    console.log('任务 ' + event.target.textContent + ' 已' + (isDone ? '完成' : '取消完成'))
  }
})
</script>
</body>
</html>

预期输出: 输入文字后点击"添加",新任务出现;点击任意任务文字,划线表示完成,再点击取消

一句话解释: 核心就在 taskList.addEventListener('click', ...) 这一行——不管你点的是哪个 li,ul 都能捕获到,这就是委托的威力。


项目 3:表格数据筛选器——组合拳

目标: 从 JSON 数据渲染一个表格,点击表头排序,点击行选中,支持动态筛选

<!DOCTYPE html>
<html>
<head>
<title>数据表格</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { cursor: pointer; background: #f5f5f5; }
tr:hover { background: #f0f0f0; }
.selected { background: #e3f2fd; }
</style>
</head>
<body>
<h1>员工信息表</h1>
<input type="text" id="filterInput" placeholder="搜索姓名或部门...">
<table id="dataTable">
<thead>
  <tr>
    <th data-sort="name">姓名</th>
    <th data-sort="department">部门</th>
    <th data-sort="salary">薪资</th>
  </tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<p id="status"></p>

<script>
// 模拟数据
const employees = [
  { name: '张三', department: '技术部', salary: 15000 },
  { name: '李四', department: '市场部', salary: 12000 },
  { name: '王五', department: '技术部', salary: 18000 },
  { name: '赵六', department: '人事部', salary: 10000 },
  { name: '钱七', department: '市场部', salary: 13000 },
]

const tableBody = document.getElementById('tableBody')
const filterInput = document.getElementById('filterInput')
const status = document.getElementById('status')
const headers = document.querySelectorAll('th')

let sortColumn = null
let sortAsc = true

// 渲染表格
function renderTable(data) {
  tableBody.innerHTML = ''
  data.forEach(function(emp, index) {
    const row = document.createElement('tr')
    row.innerHTML = `
      <td>${emp.name}</td>
      <td>${emp.department}</td>
      <td>${emp.salary}</td>
    `
    row.dataset.index = index
    tableBody.appendChild(row)
  })
  status.textContent = '共 ' + data.length + ' 条记录'
}

// 初始化
renderTable(employees)

// 事件委托:表格区域的所有点击
document.getElementById('dataTable').addEventListener('click', function(event) {
  // 点击表头排序
  if (event.target.tagName === 'TH') {
    const column = event.target.dataset.sort
    if (sortColumn === column) {
      sortAsc = !sortAsc
    } else {
      sortAsc = true
      sortColumn = column
    }

    // 排序
    const sorted = [...employees].sort(function(a, b) {
      if (sortAsc) {
        return a[column] > b[column] ? 1 : -1
      } else {
        return a[column] < b[column] ? 1 : -1
      }
    })
    renderTable(sorted)
    console.log('按 ' + column + ' ' + (sortAsc ? '升序' : '降序') + ' 排序')
  }

  // 点击行选中
  if (event.target.tagName === 'TD') {
    const row = event.target.parentElement
    row.classList.toggle('selected')
    const emp = employees[row.dataset.index]
    console.log('选中:', emp.name)
  }
})

// 筛选功能
filterInput.addEventListener('input', function(event) {
  const keyword = event.target.value.toLowerCase()
  const filtered = employees.filter(function(emp) {
    return emp.name.toLowerCase().includes(keyword) ||
           emp.department.toLowerCase().includes(keyword)
  })
  renderTable(filtered)
})
</script>
</body>
</html>

预期输出: 页面显示5个员工的表格;点击表头会排序;点击行会高亮选中;输入框输入可实时筛选

一句话解释: 一个表格,三种交互,全靠事件委托 + 一个公共父元素 document.getElementById('dataTable') 搞定。


💪 进阶:这些坑,踩过才知道疼

坑 1:动态元素的事件绑定 ❌ ✅

// ❌ 错误:只给已有的元素绑定,新添加的没有
const buttons = document.querySelectorAll('.btn')
buttons.forEach(function(btn) {
btn.addEventListener('click', handleClick)
})
// 后续添加的 .btn 点不了

// ✅ 正确:用委托,给父元素绑
document.addEventListener('click', function(event) {
if (event.target.classList.contains('btn')) {
handleClick(event)
}
})

坑 2:阻止冒泡,把娃一起泼了 ❌ ✅

// ❌ 错误:阻止冒泡后,父元素的委托也废了
child.addEventListener('click', function(event) {
event.stopPropagation()  // 父元素永远收不到了
doSomething()
})

// ✅ 正确:只在自己范围内处理
child.addEventListener('click', function(event) {
doSomething()
// 不要轻易 stopPropagation
})

坑 3:事件名写错 ❌ ✅

// ❌ 错误:用了 on 前缀
element.addEventListener('onclick', function(){})

// ✅ 正确:没有 on
element.addEventListener('click', function(){})

坑 4:忘记 event.preventDefault() ❌ ✅

// ❌ 错误:链接点击后跳走了
link.addEventListener('click', function() {
console.log('还没看到日志就跳走了')
})

// ✅ 正确:阻止默认行为
link.addEventListener('click', function(event) {
event.preventDefault()
console.log('看到了吧')
})

坑 5:异步元素 + 委托的时机 ❌ ✅

// ❌ 错误:数据还没回来就渲染了
fetch('/api/data').then(function(res) { return res.json() })
renderTable()  // 数据还在路上

// ✅ 正确:等数据回来再渲染
fetch('/api/data').then(function(res) { return res.json() }).then(function(data) {
renderTable(data)  // 数据到了再渲染
})

调试技巧:console.log 事件对象

element.addEventListener('click', function(event) {
console.log('完整事件对象:', event)
console.log('target:', event.target)
console.log('currentTarget:', event.currentTarget)
console.log('type:', event.type)
})

直接在控制台看对象结构,比猜快10倍。


✏️ 练习题

练习 1(2 分钟):改改改
- 输入:把项目 1 的初始数字从 0 改成 10
- 预期输出:页面加载时显示 10,点击按钮从 11 开始
- 提示:let count = 0 改成什么?

练习 2(3 分钟):加个判断
- 输入:在项目 1 中添加一个功能:数字超过 20 时,按钮禁用
- 预期输出:数字到 21 时,点击按钮无效
- 提示:用 btn.disabled = true

练习 3(5 分钟):新数据源
- 输入:用以下数据渲染项目 2 的待办清单:['买菜', '做饭', '洗碗']
- 预期输出:页面加载后自动有这三个任务
- 提示:在页面加载后用 forEach 创建初始 li

练习 4(5 分钟):串起来
- 输入:在项目 3 中,给表格加一列"操作",每行有个"删除"按钮
- 预期输出:点击删除按钮,该行从表格消失
- 提示:用事件委托捕获删除按钮的点击

练习 5(5 分钟):读懂报错
- 输入:以下代码运行后点击按钮,控制台报什么错?

document.querySelector('button').addEventListener('click', function(e) {
e.target.preventDefault()
})
  • 预期输出:...(评论区告诉我你的答案)
  • 提示:preventDefault 在什么对象上调用?

作业:做一个「评论系统事件处理工具」

  • 需求描述:模拟一个简单评论区,支持发表新评论、点赞、删除
  • 功能点:
    1. 输入框发表新评论,自动添加到列表顶部
    2. 每条评论有点赞按钮,点击数字+1
    3. 每条评论有删除按钮,点击后移除该评论
    4. 评论列表用事件委托处理所有交互
  • 加分项:
    1. 本地存储刷新不丢数据(localStorage)
    2. 点赞数实时保存
  • 验收标准:能跑起来 + 3个功能都能用 + 代码有注释
  • 提交方式:评论区贴代码或 GitHub 链接

📚 总结

这一章学了 3 件事:

  1. addEventListener —— 更专业的事件绑定,支持多函数、不覆盖
  2. 事件对象 —— 包含 target、preventDefault、stopPropagation 这些法宝
  3. 事件委托 —— 精髓是用父元素代理子元素,动态新增的元素也能自动响应

下一章我们要解决一个新问题:网页学会了交互,但数据从哪来?总不能每次都手写吧——下一章「Fetch API 与 AJAX」,教你从服务器要数据!


推荐资源:
- MDN 官方文档:Events 指南
- 视频:JavaScript 事件委托教程(B站搜"事件委托",选播放量最高的)
- 书籍:《JavaScript 高级程序设计》第4版 第13章


互动钩子: 你在项目里用过事件委托吗?遇到过什么奇怪的bug?评论区聊聊,帮你诊断!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。