JavaScript 事件委托
上一篇文章讲了事件冒泡与捕获,事件会从目标元素一路冒泡到祖先元素。这个特性带来一个巧妙的用法,事件委托。把监听绑在父元素上,让父元素统一处理所有子元素触发的事件。
事件委托的核心思路是利用冒泡,不给每个子元素单独绑定,只在父元素上绑一次。
事件委托是什么
假设页面上有一个列表,里面有多个 li,要求点击任意 li 都输出它的文字。逐个绑定当然可以,但更简洁的做法是在 ul 上绑一次。
<ul id="list">
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
<script>
const list = document.getElementById('list')
list.addEventListener('click', (e) => {
console.log(e.target.textContent)
})
</script>
点击任意 li,事件冒泡到 ul,ul 上的监听执行,通过 e.target 拿到真正被点击的 li。这就是事件委托,一个监听,处理所有子元素。
判断点击的是哪个子元素
事件委托的监听绑在父元素上,需要用 e.target 判断实际点击的元素。列表里可能混有别的元素,判断一下更稳妥。
<ul id="list">
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
<script>
const list = document.getElementById('list')
list.addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log(`你点击了 ${e.target.textContent}`)
}
})
</script>
e.target.tagName 返回元素标签名的大写形式,比较时用全大写。也可以改用 matches 方法匹配选择器,语义更清晰。
list.addEventListener('click', (e) => {
if (e.target.matches('.fruit')) {
console.log(`你点击了 ${e.target.textContent}`)
}
})
动态元素的天然优势
事件委托最大的优势是处理动态生成的元素。给每个 li 单独绑定的写法,对新加的 li 无效,新元素根本没有绑定过监听。用 appendChild 或 innerHTML 新增的 li 必须重新绑定才能响应,代码很快变得繁琐。
<ul id="list"></ul>
<button id="add">添加一项</button>
<script>
const list = document.getElementById('list')
const add = document.getElementById('add')
add.addEventListener('click', () => {
const li = document.createElement('li')
li.textContent = `第 ${list.children.length + 1} 项`
list.appendChild(li)
})
list.addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log(`你点击了 ${e.target.textContent}`)
}
})
</script>
监听加在 ul 上,ul 一直存在,新 li 的事件冒泡到 ul 时同样能触发,不需要任何重新绑定。对比之下,给每个 li 单独绑定的写法需要在新 li 生成时补一次绑定,否则新 li 点击没有反应,维护成本明显更高。
实操,待办列表删除功能
把前面的知识拼起来,做一个完整的待办列表。输入框输入内容,点击添加按钮生成 li,每条 li 带一个删除按钮,用事件委托在 ul 上绑一次 click 实现删除。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>待办列表</title>
<style>
body { font-family: sans-serif; max-width: 400px; margin: 40px auto; }
.input-bar { display: flex; gap: 8px; margin-bottom: 16px; }
input { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { padding: 8px 16px; border: none; border-radius: 4px; background: #4caf50; color: #fff; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; margin-bottom: 8px; background: #f5f5f5; border-radius: 4px; }
.del { background: #e74c3c; padding: 4px 10px; }
</style>
</head>
<body>
<div class="input-bar">
<input id="task" type="text" placeholder="输入待办事项">
<button id="add">添加</button>
</div>
<ul id="list"></ul>
<script>
const taskInput = document.getElementById('task')
const addBtn = document.getElementById('add')
const list = document.getElementById('list')
addBtn.addEventListener('click', () => {
const text = taskInput.value.trim()
if (text === '') {
return
}
const li = document.createElement('li')
li.innerHTML = `<span>${text}</span><button class="del">删除</button>`
list.appendChild(li)
taskInput.value = ''
})
list.addEventListener('click', (e) => {
if (e.target.matches('.del')) {
e.target.closest('li').remove()
}
})
</script>
</body>
</html>
删除逻辑只有几行,关键在委托。点击删除按钮,事件冒泡到 ul,监听判断 e.target 是不是删除按钮,是则向上找到所属的 li 并移除。新添加的 li 自带删除按钮,不需要单独绑定任何事件,天然可用。
这段代码结合了创建与删除 DOM 元素和事件委托两课的知识。整个列表只有一个监听,无论添加多少项,删除功能始终有效。
小结
事件委托是事件冒泡的典型应用,在父元素上绑定一次监听,统一处理子元素触发的事件。用 e.target 判断实际点击的元素,tagName 和 matches 都可以用。动态生成的元素天然支持事件委托,省去反复绑定的麻烦。待办列表删除功能是事件委托的经典实操场景。