🔥事件委托?花4min看完这篇文章就会了🔥

99 阅读8分钟

前言

在现代前端开发中,事件处理是实现用户交互的关键环节。无论是传统的DOM操作还是现代的React应用,事件处理机制都是不可或缺的一部分。然而,随着应用复杂度的增加,传统的事件处理方式在性能和维护性上逐渐显现出不足。React作为目前最流行的前端框架之一,引入了 合成事件(Synthetic Event)事件委托(Event Delegation) 等机制,极大地优化了事件处理的性能和用户体验。

本文和下文将深入探讨JavaScript的事件机制基础,包括DOM事件的不同级别、事件监听方法以及事件流模型。接着,我们将重点介绍React中的合成事件和事件池概念,理解其在性能优化上的优势。

事件交互

首先我们介绍一下事件交互,即HTML如何与JS配合进行事件的交互行为。

最开始是HTML事件:

HTML事件是指在HTML标签中直接通过事件属性(如 onclickonmouseover 等)来指定JavaScript代码的方式。这种方式最早出现在HTML 4.01中。

<button onclick="alert('按钮被点击了!')">点击我</button>

虽然能够直观地看到事件处理逻辑,但是嵌入到这里的逻辑会难以维护(当逻辑过长时),而且它耦合了,没有遵从HTML、CSS和JavaScript代码分离的原则。

所以我们后来发展了 DOM0 事件:

DOM0事件是指通过JavaScript直接在元素上设置事件处理程序的方法。这种方法使用元素的属性(如 element.onclick)来绑定事件处理器。

<button id="myButton">点击我</button>

<script>
  document.getElementById('myButton').onclick = function() {
    alert('按钮被点击了!');
  };
</script>

它成功实现了代码逻辑分离,可以在运行的时候动态地添加或者删除事件逻辑,但是每个事件类型只能有一个处理函数。如果多次设置同一个事件类型的处理函数,后面的会覆盖前面的。

var btn = document.getElementById('btn');
        btn.onclick = function(){
            alert("Click1")
        }
        btn.onclick = function(){
            alert("Click2!") // 覆盖前面的
        }

所以我们又发展了强大的DOM2级事件:

DOM2事件模型引入了更为强大的事件处理机制,允许在一个元素上绑定多个事件处理程序,并支持事件捕获和冒泡阶段。主要通过 addEventListenerremoveEventListener 方法来实现。

<button id="myButton">点击我</button>

<script>
  const myButton = document.getElementById('myButton');

  function handleClick() {
    alert('按钮被点击了!');
  }
  // 添加事件监听器
  myButton.addEventListener('click', hanleClick);

  // 添加第二个事件监听器
  myButton.addEventListener('click', function() {
    console.log('另一个点击事件');
  });

  // 移除第一个事件监听器
  myButton.removeEventListener('click', handleClick);
</script>

事件委托

接下来我们来介绍一下事件委托:

事件委托(Event Delegation) 是一种JavaScript编程模式,主要用于提高性能和简化代码。其核心思想是不在每个子元素上直接绑定事件监听器,而是将事件监听器绑定到这些子元素的共同父元素上。 当事件发生在某个子元素时,它会冒泡到父元素,在那里被统一处理。这样做的好处包括减少内存使用、易于管理和维护动态生成的内容等。

假如我要给列表的每一个元素加监听,监听是否被点击了,不用事件委托是这样的:

<body>
<ul id="list">
  <li>项目 1</li>
  <li>项目 2</li>
  <li>项目 3</li>
  <!-- 可能会有更多项目 -->
</ul>

<script>
const items = document.querySelectorAll('#list li');
items.forEach(item => {
  item.addEventListener('click', function() {
    console.log(this.textContent + ' 被点击了!');
  });
});

</script>
</body>

用了事件委托后:

<body>
<ul id="list">
  <li>项目 1</li>
  <li>项目 2</li>
  <li>项目 3</li>
  <!-- 可能会有更多项目 -->
</ul>

<script>
document.getElementById('list').addEventListener('click', function(event) {
  if (event.target && event.target.nodeName === 'LI') {
    console.log(event.target.textContent + ' 被点击了!');
  }
});
</script>
</body>

在这里我们少用了很多监听器,数据量小的情况下,性能可能没什么,如果这个列表是100万条数据?1000万条数据?如果每一个都加上监听器,太耗费性能了,所以我们选择利用事件委托,给其父元素添加监听器来解决这个问题。

那么在这里就不得不提事件委托的原理——————冒泡机制了。

冒泡机制

冒泡机制(Event Bubbling) 是浏览器处理事件的一种机制,它指的是当一个事件在某个元素中触发时,会从事件目标元素开始,逐级向上(外层父元素)传播,直到达到文档根节点(如document或window)。这种传播方式像水中的气泡一样,从底部上升到水面,被称为“冒泡”。

冒泡机制有三个阶段:

1. 事件捕获阶段(Capturing Phase)

事件从文档的根节点向下传播到事件目标元素

这一阶段可以通过addEventListener的第三个参数设置为true来监听。

(默认为false则监听冒泡阶段,true为监听捕获阶段)

2. 目标阶段(Target Phase)

事件到达事件目标元素并触发。

3.冒泡阶段(Bubbling Phase)

事件从事件目标元素向上逐级传播到文档的根节点。

这一阶段是默认的,addEventListener第三个参数默认为false

冒泡机制的作用

1. 简化事件的处理

可以在父元素上统一处理子元素上的事件,而不需要为每个子元素单独绑定事件处理函数。

例如,可以在表格的父元素上监听点击事件,而不需要为每个单元格绑定事件。

2. 提高性能

减少事件处理函数的数量,降低内存消耗。

3. 支持事件委托 (Event Delegation)

利用冒泡机制,可以将事件处理委托给父元素,动态处理子元素的事件。

就拿下面的例子来讲:

<div class='grandma'>
    grandma-奶奶
	<div class='mother'>
        mother-妈妈
        <div class='daughter'>
            daughter-女儿
            <div class='baby'>
                baby-婴儿
            </div>
        </div>
    </div>    
</div>
var grandma = document.getElementByClassName("grandma")[0];
var mother = document.getElementByClassName("mother")[0];
var daughter = document.getElementByClassName("daughter")[0];
var baby = document.getElementByClassName("baby")[0];

function theName(){
    console.log("我是 "+this.className)
}
baby.addEventListener('click',theName);
daughter.addEventListener('click',theName);
mother.addEventListener('click',theName);
grandma.onclick = theName;

当我们点击 baby-婴儿的时候会触发什么呢?

这个时候,浏览器创建事件对象,它知道了是一个click事件,也知道了是哪里触发的,确定了目标元素, 之后事件开始被捕获,这个事件从文档根节点一直向下传播,一直传播到被点击的元素baby,之后到了目标阶段,事件到达目标并触发,baby触发了click事件,之后就到了冒泡阶段,baby触发的click事件从baby沿着父元素逐级向上传播,它的父元素全部触发了click事件,最后到达文档根节点。

这样,我们点击baby后会输出:

我是baby
我是daughter
我是mother
我是grandmother
image.png

事件捕获?事件冒泡?

所以这两个是怎么被监听到的呢?其实都和addEventListener有关: Element.addEventListener(event,listener,useCapture)接收三个参数:

  • 第一个参数:event,是触发了什么事件,在这里是click事件,还有很多事件,比如keyup,doubleclick,keydown....
  • 第二个参数:listener,接收一个函数,当触发事件的时候会执行这个函数,它会接收一个参数event,值为触发的事件,我们可以用e.target来访问触发事件的元素
  • 第三个参数:是一个bool值,true/false,默认为false. true的时候监听事件捕获阶段,false的时候监听事件冒泡阶段

阻止冒泡

当然我们也可以阻止冒泡,有的时候一直冒泡不会达到我们想要的结果,比如:

当我们在父元素中有个模态框,子元素也有模态框,我们在他们俩身上都设置了点击事件,那么点击子元素的模态框唤起功能会唤起父元素的模态框唤起(这里只是一个情景,现实中不会有傻憨憨这么开发/......)

那么就需要阻止冒泡了:

btn.onclick = function (e){
    e.stopPropagation();//阻止事件继续冒泡
    console.log('btn');
}

阻止默认行为

默认行为是指浏览器在特定事件发生时自动执行的操作。这些操作通常是浏览器为了提供一致的用户体验而预设的。例如,当你点击一个链接时,浏览器会默认跳转到该链接指向的页面;当你提交一个表单时,浏览器会默认将表单数据发送到服务器。这些自动执行的操作就是默认行为。

e.preventDefault() 是 JavaScript 中用于阻止元素的默认行为的方法。它通常在事件处理函数中使用,当某个事件触发时,可以调用 e.preventDefault() 来阻止浏览器执行该事件的默认操作。

假设你有一个表单,你希望在提交前进行一些验证,并且在验证未通过时不提交表单:

<form id="myForm">
  <input type="text" name="username" required>
  <button type="submit">提交</button>
</form>
<script>
  document.getElementById('myForm').addEventListener('submit', function(e) {
    // 获取输入值
    const username = e.target.username.value;
    // 检查输入是否为空
    if (username === '') {
      alert('用户名不能为空');
      e.preventDefault();  // 阻止表单提交
    }
  });
</script>

在这个例子中,如果用户名为空,e.preventDefault() 会阻止表单的提交。

总结

我们先介绍了HTML事件、DOM0事件和DOM2事件的发展历程,以及它们在现代前端开发中的应用。通过对比不同事件处理方式的优缺点,强调了DOM2事件模型的灵活性和性能优势。此外,还详细讲解了事件委托和冒泡机制,展示了如何利用这些机制优化事件处理,提高代码的可维护性和性能。最后,讨论了如何使用 e.preventDefault()e.stopPropagation() 来控制事件的默认行为和传播,确保更好的用户体验和逻辑控制,下一篇————React的事件机制!