前端小知识事 件 委 托
1. 引例
考虑下面一个问题:
- 当我们在前端开发中遇到需要 为一组相似的元素绑定事件处理程序 的情况时,通常会面临一个问题:为每个元素都创建一个独立的事件处理函数——这种方式是否是好?
我们将通过下面这个例子来说明这个问题和委托的概念。
【问题描述】:
- 假设我们有一个网页,其中包含一个购物车,里面有很多商品项,每个商品项都有一个"添加到购物车"按钮。
- 我们希望当用户点击这些按钮时能够执行相同的操作,即将商品添加到购物车,并更新购物车的显示。
一种最简单的方式是为每个按钮分别绑定点击事件处理程序,就像这样:
<ul id="cart"> <li> <span>商品1</span> <button class="add-to-cart">添加到购物车</button> </li> <li> <span>商品2</span> <button class="add-to-cart">添加到购物车</button> </li> <!-- 更多商品项... --> </ul> <script> const addToCartButtons = document.querySelectorAll('.add-to-cart'); addToCartButtons.forEach(button => { button.addEventListener('click', function() { // 执行添加到购物车的逻辑 // 更新购物车显示 }); }); </script>
尽管上述代码在功能上是可行的,但它存在一个潜在问题:
- 为每个按钮都创建一个单独的事件处理函数。
- 可以想象——当有大量商品时会导致创建大量相似的函数实例,占用大量内存。
2. 事件冒泡
事件冒泡(Event Bubbling)是指在处理DOM事件时,事件会从触发它的最内层元素开始冒泡,逐级向上传播,一直传递到根元素(通常是或),直到被停止或取消。这意味着如果一个子元素触发了某个事件,那么这个事件也会逐级传递给该子元素的父元素,以及父元素的父元素,以此类推。
事件冒泡的工作流程如下:
- 首先,用户在页面上的某个元素上触发了一个事件,比如点击鼠标或触摸屏幕;
- 事件首先被分派到触发事件的 最内层元素(目标元素),并在该元素上 执行绑定的事件处理程序。
3.然后,事件开始冒泡,向上级元素传递,一层一层地触发父元素上的相同事件;
4.当事件到达文档根部(通常是元素)时,它可能会 停止冒泡,也可能继续传递到浏览器层面,(根据事件是否被取消来决定)。
在这个过程中,任何父元素上绑定的事件处理程序都可能被触发,包括目标元素本身的父元素、爷爷元素、曾祖父元素,以及文档根元素等。
3. 事件冒泡是委托的实现依据
在第1小节的案例中,我们提到了一个小缺陷。也iu是,为每个按钮都创建一个单独的事件处理函数,可能导致大量商品时会导致创建大量相似的函数实例,占用大量内存。
这时,我们联想起冒泡机制:
- 冒泡机制使得事件 可以在DOM结构中传递并被多个元素捕获。这允许我们 在父元素上捕获子元素的事件,从而减少事件处理程序的数量,提高性能和可维护性。
事实上,冒泡机制也是事件委托模式的基础。
结合第1小节的案例来看,当用户点击任何一个"添加到购物车"按钮时,事件会冒泡到
- 元素,我们可以在父元素上处理事件,修改被修改的那部分代码如下:
<script> const cart = document.getElementById('cart'); cart.addEventListener('click', function(event) { if (event.target.classList.contains('add-to-cart')) { // 执行添加到购物车的逻辑 // 更新购物车显示 } }); </script>
修改后的脚本相比于第1小节中的脚本的主要不同之处在于——事件处理的位置。
在第1小节中,事件处理程序是直接绑定在每个 “添加到购物车” 按钮上,使用了 button.addEventListener
。这意味着每个按钮都有自己的独立事件处理程序,当用户点击其中一个按钮时,只会触发与该按钮关联的事件处理程序:
addToCartButtons.forEach(button => { button.addEventListener('click', function() { // 执行添加到购物车的逻辑 // 更新购物车显示 }); });
而修改后的脚本,事件处理程序是绑定在购物车容器
- 上,使用了
cart.addEventListener
。然后,在事件处理程序内部,通过检查event.target
来确定触发事件的元素是否包含类名 ‘add-to-cart’。如果是,就执行相应的逻辑:
const cart = document.getElementById('cart'); cart.addEventListener('click', function(event) { if (event.target.classList.contains('add-to-cart')) { // 执行添加到购物车的逻辑 // 更新购物车显示 } });
这样修改有什么好处呢:
- 前者每个按钮都有自己的事件处理程序,这在逻辑上更容易理解,但当有大量按钮时,会导致创建多个函数实例,可能占用更多内存。
- 后者只有一个事件处理程序绑定在购物车容器上,这可以减少内存占用,提高性能,因为只有一个事件处理程序实例。
后者这种利用事件的冒泡特性,将多个子元素的同一类型的监听器合并到父元素上来实现的方式就被称作所谓的事件委托。
4. 另一个场景:动态生成的元素的事件绑定
事件委托的思想还可以应用于 监听 动态生成的元素和动态绑定事件。
动态生成的元素 是指 在页面加载后 通过JavaScript代码创建的元素——由于这些元素并不存在于初始HTML中,因此不能像静态元素那样直接绑定事件处理程序。
让我们通过一个具体的例子来详细讲解——如何使用事件委托来监听和处理动态生成的元素的事件。
假设我们有一个按钮,点击它将在页面上动态创建一个新的列表项(<li>元素),然后我们希望能够点击这些动态创建的列表项并执行一些操作,比如改变它们的样式或进行其他操作。代码如下。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>动态元素事件委托案例</title> </head> <body> <ul id="dynamic-list"> <!-- 这里没有任何<li>元素 --> </ul> <button id="add-button">添加列表项</button> <script> const dynamicList = document.getElementById('dynamic-list'); const addButton = document.getElementById('add-button'); // 点击按钮时,动态创建一个新的列表项 addButton.addEventListener('click', function() { const newItem = document.createElement('li'); newItem.textContent = '新的列表项'; dynamicList.appendChild(newItem); }); // 事件委托,监听<ul>上的点击事件 dynamicList.addEventListener('click', function(event) { if (event.target.tagName === 'LI') { // 当点击列表项时,执行操作 event.target.style.backgroundColor = 'lightblue'; } }); </script> </body> </html>
本例中,有一个 添加按钮(addButton)和一个空的 无序列表(dynamicList)。
- 当用户点击按钮时,动态创建一个新的列表项(<li>元素),并将其添加到列表中。
- 我们使用事件委托将点击事件绑定到<ul>元素上,监听了所有<li>元素的点击事件。
无论何时点击动态生成的列表项,事件都会冒泡到<ul>元素,事件处理程序检查被点击的元素是否是<li>元素,如果是,就执行相应的操作。在本例中,我们改变了被点击的列表项的背景颜色。效果如图所示:
从本例可以看到,这种方式下,我们能够动态绑定事件处理程序而不需要为每个列表项都手动绑定——因此,无论有多少个列表项,代码都能完成处理。