第6章 6.2 事件处理与委托
🎯 开场:为什么你点的外卖总送错?
你有没有遇到过这种情况——在网上选了商品加入购物车,点了结算,结果页面毫无反应?或者一个按钮点下去,弹出了两个弹窗?
这种"我明明点了,系统不响应"或者"响应一次变两次"的bug,80% 都跟事件处理有关系。
上一章我们学了 querySelector 和基本的事件绑定(onclick = function()),但那种写法有个致命问题:每次动态添加新元素,还得重新绑一遍。
举个例子——你有100个商品卡片,每个卡片里有个"加入购物车"按钮。用上一章的方法,你得循环100次,手动给每个按钮绑事件。但如果你后续又动态加载了20个新商品呢?新按钮又得重新绑...
这一章我们要解决的就是这个痛点。学完之后,你就能写出"加一个新元素进去,自动就有交互"的代码——这就是事件委托的核心威力。
🧱 基础:事件处理到底是怎么回事?
6.2.1 重新认识「事件」
先把一个认知扳过来。
事件不是「点一下就触发」这\n\n
\n\n
\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 件事:
addEventListener—— 更专业的事件绑定,支持多函数、不覆盖- 事件对象 —— 包含 target、preventDefault、stopPropagation 这些法宝
- 事件委托 —— 精髓是用父元素代理子元素,动态新增的元素也能自动响应
下一章我们要解决一个新问题:网页学会了交互,但数据从哪来?总不能每次都手写吧——下一章「Fetch API 与 AJAX」,教你从服务器要数据!
推荐资源:
- MDN 官方文档:Events 指南
- 视频:JavaScript 事件委托教程(B站搜"事件委托",选播放量最高的)
- 书籍:《JavaScript 高级程序设计》第4版 第13章
互动钩子: 你在项目里用过事件委托吗?遇到过什么奇怪的bug?评论区聊聊,帮你诊断!

评论(0)