JS事件机制详解(2)--- 委托机制、事件应用

前言

大家好,这里是JS事件机制讲解的第二篇,之前我们在第一篇中详解了事件流中的捕获阶段、目标阶段、冒泡阶段。这篇文章会仔细讲讲JS原生事件中的委托机制,以及一些事件机制的应用,相信看完这一篇后能够将JS的事件机制理解推向一个全新的高度,达到大厂的水平!

何为事件委托?

请你查看下面的场景

这是一段html代码,我们的父元素myList里面放着多个子元素li,它们的自定义属性data-item的值互不相同。

html 复制代码
<div id="root">
    <ul id="myList">
    <li data-item="123">Item 1</li>
    <li data-item="456">Item 2</li>
    <li data-item="789">Item 3</li>
    <li data-item="012">Item 4</li>
    </ul>
</div>

现在我们想要为myList里面的每一个li元素添加一个效果:点击某个li元素,会输出它的内容。

请你想想?我们该如何做?

我们是不是应该为每一个li元素添加一个click的addEventListener。

js 复制代码
document.querySelectorAll('#myList li').forEach(item => {
  item.addEventListener('click', () => {
    console.log(item.dataset.item);  //输出它的自定义属性
  });
});

查看效果:

可以看到这种方式是有效的,确实能够输出每个元素的data-item

让我们结合上一篇的文章的事件流过程进行分析这里的过程:

在这里,事件从 windowdocument#root#myList -> li 向下捕获,到了目标阶段,会到达每个实际被点击的 li 元素,因此该元素上的事件的监听器会触发,然后事件从 li#myList#root 向上冒泡,依次触发冒泡的元素的监听器。

这种方式性能太差了

但是我们完全没有必要这样做,因为这种为每一个子元素都添加一个事件监听的方式性能很差

接下来,让我们用事件委托来进行优化:

事件委托事件委托 是利用事件冒泡机制,将子元素的事件监听统一委托给父元素处理。通过 event.target 判断实际触发元素,减少内存消耗,提升性能。

所以在这个案例中,我们可以改为仅为父元素myList设置事件监听 ,让它统一处理各个子元素li的内容输出

js 复制代码
 document.getElementById('myList').addEventListener('click',function(event){
        console.log(event.target);
    })

这样做的话,当我们点击某个item时在冒泡阶段

  • 从子元素开始,首先没有发现子元素li的事件监听,继续往上冒泡

  • 冒泡到父元素myList,发现其有事件监听,于是执行它的事件函数:打印event.target

此时: 这里的event.target始终指向最初被点击的<li>,就是我们点击的某个具体元素

所以利用这种事件委托的方式,同样可以达到我们想要的效果,并且它的性能大大提升:

  • 仅需一个监听器,就可以达到传统使用N个监听器同样的效果
  • 内存占用恒定,不随子元素数量增加

同时,而且如果想要对某个元素进行限定,可以利用下面这种方式

js 复制代码
document.getElementById('myList').addEventListener('click',function(event){
    console.log(event.target.dataset.item); 
    // 对456这个元素进行限定
    if (event.target.dataset.item === '456'){
            console.log('Im Item2!!!')
        }
    })

事件委托的必要条件:唯一值

在事件委托中,对于各个子元素,我们需要为每个子元素设置一个唯一值,如同上面的item1->item4,你可以看见我们设置了自定义属性data-item 为"123"、"456"、...这样,必须这样设置唯一值的原因如下:

1. 精确识别目标元素

当使用事件委托时,事件处理程序是绑定在父元素上的,但我们需要知道事件实际发生在哪个子元素上。唯一值可以帮助我们:

  • 区分不同的子元素
  • 精确识别用户实际交互的元素
  • 避免处理不相关的子元素事件

2. 性能优化

  • 避免为每个子元素单独绑定事件处理程序
  • 通过唯一值可以快速查找和定位目标元素
  • 减少内存使用(只需一个事件处理程序)

事件委托的就近原则

每个事件委托中,理论上,子元素不仅可以委托到父元素,还可以委托到父元素的父元素,比如li的父元素为myList,body同样也可以称为li的父元素,由于冒泡阶段会一直往外冒泡,所以上面的例子我们选择body或者document都是没有问题的,当选择了某个父元素时,我们称这个父元素代理了子元素,父子元素共同形成了一个事件委托

但是事件委托有个原则--就近原则:

理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,以提高性能。比如推荐在myList上代理li,而不是在document上代理li

事件委托应用:动态添加元素

让我们对刚才的案例进行变更:

现在,我们想要在页面上添加一个按钮,用户点击按钮时能够增加一个myList里面的元素li

html 复制代码
<div id="root">
    <ul id="myList">
    <li data-item="123">Item 1</li>
    <li data-item="456">Item 2</li>
    <li data-item="789">Item 3</li>
    <li data-item="012">Item 4</li>
    </ul>
    
    <div id="container" data-item="ab1">hello</div>
    <button id="btn">添加节点</button>
</div>

我们需要实现:对于新添加的li,点击后会输出这个li的相关内容。

在之前,你可能会说,直接新添一个元素并且对这个新元素注册一个事件click不就好了呗

但是今天在学了委托机制后,你是否有了新的思路呢?

我们完全可以仅为父元素注册一个事件监听就可以完成上述效果!

js 复制代码
    document.getElementById('myList').addEventListener('click',function(event){
        console.log(event.target.innerText)
    })
    document.getElementById('btn').addEventListener('click',function(event){
        // 动态添加元素
        const newLi = document.createElement('li');
        newLi.appendChild(
            document.createTextNode('item-new')
        )
        document.getElementById('myList').appendChild(
            newLi
        )
    })

因此,我们只需要对该按钮绑定一个添加元素的事件即可,对新添加的元素,无需注册事件。 这就是事件委托在动态添加元素上的应用

对于这个案例,先给大家看看我们想要实现的效果:

  1. 点击Toggle Menu控制menu的显示开关

2.点击页面任意空白处可以关闭menu的显示

这是一种多见的设计,我们平常在上网时,会经常见到这种设计,举个例子,比如掘金

点击发布按钮可以控制发布菜单的展开与收回,点击页面任意处可以收回已展开的发布菜单

接下来,让我们来实现这种效果:

1.写html和css

html 复制代码
<style>
    #menu {
        position: absolute;
        top: 50px;
        left: 50px;
        padding: 20px;
        background-color: #f2f2f2;
        border: 1px solid #ccc
    }
    #toggleBtn {
        cursor: pointer;
    }
</style>

<div id="toggleBtn">Toggle Menu</div>

<div id="menu">
    <p>Menu Context</p>
    <a href="https://www.baidu.com" id="closeInside" style="display: none">Don't closed</button>
</div>

可以看到,默认这个menu组件是不显示的(display: none)

2.书写js

在js中,首先我们先实现:

点击tooggleBtn时可以控制menu的控制与收回,注册一个事件即可

js 复制代码
const toggleBtn = document.getElementById('toggleBtn')
        const menu = document.getElementById('menu')
        const closeInside =document.getElementById('closeInside')

        toggleBtn.addEventListener('click',function(e){
            menu.style.display = menu.style.display === 'block'?'none':'block'
        })

现在就可以通过点击Toggle Menu这行文字控制menu的显示与收回了

接下来我们要实现:

点击页面任意空白处能够将已显示的menu给收回

我们这样做:

js 复制代码
document.addEventListener('click',function(){
            menu.style.display = 'none'
        })

但是这个时候你会发现,点击Toggle Menu后,menu打不开了!

这是因为:

在冒泡阶段 从里向外冒泡,先执行里面的这个事件menu.style.display = menu.style.display === 'block'?'none':'block',然后再执行外面的menu.style.display = 'none'事件,外面的事件后执行,所以无论如何menu都不会显示!!!

所以我们这种写法有误,正确的写法应该是此时为子元素添加e.stopPropagation()

js 复制代码
//修改后,添加e.stopPropagation()
toggleBtn.addEventListener('click',function(e){
            e.stopPropagation()
            menu.style.display = menu.style.display === 'block'?'none':'block'
        })

请问e.stopPropagation()是什么?有什么作用?

e.stopPropagation() 是 JavaScript 中用于处理事件冒泡的方法,e.stopPropagation() 的作用是阻止事件继续向上冒泡,即事件只会触发当前元素的处理函数,而不会传播到其父元素。

所以我们这里用e.stopPropagation()能够很好地完成效果:点击子元素时,会阻止父元素的事件发生,只执行子元素的事件!

这种写法是一种常见的最佳实战,对于不想冒泡到外面的子元素,可以使用这种写法轻松实现,而且也不会影响父元素事件本身的执行。

所以这就是事件机制在这种Toggle Menus上的应用!

拓展:现代react的合成事件SyntheticEvent

上面我们介绍了的是JS原生的事件机制,下面介绍下React的事件机制,React 的事件机制是基于浏览器原生事件系统的封装,但进行了优化和跨浏览器兼容性处理。以下是 React 事件机制的核心特点和工作原理:

1. 合成事件(SyntheticEvent)

React 使用合成事件(SyntheticEvent) 统一不同浏览器的原生事件差异。所有事件都是通过SyntheticEvent的实例传递的,它是浏览器原生事件的跨浏览器包装器,具有与原生事件相同的接口(如stopPropagation()preventDefault()),但行为一致。

js 复制代码
<button onClick={(e) => {
  e.preventDefault(); // 合成事件的方法
  console.log(e.nativeEvent); // 访问底层原生事件
}}>点击</button>

2. 事件委托(Event Delegation)

React 会自动帮我们实现事件委托:它不会将事件直接绑定到具体的 DOM 节点,而是通过事件委托 将所有事件统一绑定到document(React 17 之前)或根 DOM 节点(React 17+)。这样减少了内存消耗,并能动态处理新增/删除的组件事件。

  • React 16及之前 :事件委托到document
  • React 17+ :事件委托到ReactDOM.render绑定的根节点(如root),避免多版本 React 共存时的问题。

3. 事件池(Event Pooling)

React 17 之前,合成事件对象会被放入事件池 复用,事件回调执行后,事件对象的属性会被清空。若需异步访问事件(如setTimeout),需调用e.persist()保留事件。

javascript 复制代码
// React 16 及之前需要 e.persist()
<button onClick={(e) => {
  e.persist(); // 保留事件对象
  setTimeout(() => console.log(e.target), 100);
}}>Click</button>

React 17+ 移除了事件池机制 ,无需再调用e.persist()

4. 事件命名规则

React 事件使用驼峰命名 (如onClickonMouseDown),而非原生 DOM 的小写(如onclick)。

总结:React 事件机制的优势

  1. 跨浏览器一致性 :通过SyntheticEvent屏蔽浏览器差异。
  2. 性能优化:事件委托减少内存占用。
  3. 声明式语法:与组件生命周期无缝集成。

理解这些机制有助于避免常见陷阱(如异步访问事件对象、this绑定问题等)。

总结

以上就是JS事件机制的全部内容了,通过这两篇事件机制详解的文章,我们可以对JS的事件机制做一个全面的认识与掌握,最后还简单下介绍了React的现代合成事件,它是如此的震撼!如果对React的合成事件机制感兴趣的话,可以去阅读React的一些官方文档,这篇文章系列主要是介绍JS原生的事件机制,作者后期可能也会更新React的合成事件机制,如果感兴趣的话,可以点个关注呀,谢谢大家!

相关推荐
lichenyang4537 分钟前
Vue状态管理工具pinia的使用以及Vue组件通讯
前端
腹黑天蝎座8 分钟前
如何更好的封装一个接口轮询?
前端·react.js
AlenLi8 分钟前
JavaScript - 观察者模式的实现与应用场景
前端·设计模式
siroi11 分钟前
【nginx】NJS 的简单实践
前端
cxyxiaokui00111 分钟前
线程池的“变形记”:核心线程数居然能随时变大变小?
java·面试
饮水机战神13 分钟前
震惊!多核性能反降11%?node接口压力测试出乎意料!
前端·node.js
一只叁木Meow14 分钟前
JavaScript数学库深度对比
前端
顾辰逸you16 分钟前
uniapp--咸虾米壁纸项目(一)
前端·微信小程序
方方洛30 分钟前
电子书阅读器:epub电子书文件的解析
前端·产品·电子书
idaibin31 分钟前
Rustzen Admin 前端简单权限系统设计与实现
前端·react.js