六维教程

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 都可以用。动态生成的元素天然支持事件委托,省去反复绑定的麻烦。待办列表删除功能是事件委托的经典实操场景。

上一篇
JavaScript 事件冒泡与捕获
下一篇
ES6 教程