Skip to content

事件委托

事件委托也叫事件代理,一种高性能事件处理模式

简单来说,不在子元素(比如每个<li>)上单独设置事件监听器,而是把事件监听器设置在它们共同的父元素(比如<ul>)上,由父元素统一监听和处理子元素的事件

底层原理

得益于浏览器的**冒泡事件(Event Bubbling)**机制

当你在页面上点击一个按钮时,点击事件不仅发生在这个按钮上,还会像水底的气泡一样,一层一层往上传递给它的爸爸、爷爷,直到最外层的 documentwindow

例如

  • 用户点击了最里面的子元素<ul>
  • 事件向上传递,触发父元素<ul>的点击事件
  • 时间继续传递,触发爷爷元素<div id="app">的点击事件
  • 一直向上 传递到<body><document>,最后到window

示例

传统方法

js
// 如果列表有 1000 个商品,浏览器要在内存里创建 1000 个监听器函数
const items = document.querySelectorAll('li');
items.forEach(item => {
  item.addEventListener('click', function() {
    console.log(this.innerText);
  });
});

缺点:浪费内存,且如果后续通过JS动态新增了一个<li>,这个新元素没有绑定事件,点击毫无反应

优化方法:事件委托只给父元素绑定

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>10</li>
    </ul>

    <script>
      const ul = document.querySelector('ul')

      ul.addEventListener('click', (e) => {
        if (e.target.tagName === 'LI') {
          console.log(e.target.innerText)
        }
      })
    </script>
  </body>
</html>

注意事项

嵌套子元素的处理

如果你的 <li> 里面还包了其他标签(比如 <li><span>商品名</span><i>图标</i></li>),用户点击了 <span> 时,event.target 就会变成 SPAN,此时上面的 event.target.tagName === 'LI' 判断就会失效

解决方案:使用 element.closest() 方法,向上寻找最近的符合条件的祖先元素:

js
list.addEventListener('click', function(event) {
  // 寻找点击元素本身或其祖先中最近的 li 标签,直到找到 ul 为止
  const li = event.target.closest('li');
  
  // 确保找到了 li,并且这个 li 确实属于当前的 ul(防止跑到外面去)
  if (li && list.contains(li)) {
    console.log(li.innerText);
  }
});

不是所有事件都能委托

不支持冒泡的常见事件:focusblurmouseentermouseleave

如果非要委托 focus/blur,可以使用它们对应的支持冒泡的兄弟事件:focusinfocusout