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的合成事件机制,如果感兴趣的话,可以点个关注呀,谢谢大家!

相关推荐
花生侠23 分钟前
记录:前端项目使用pnpm+husky(v9)+commitlint,提交代码格式化校验
前端
猿榜25 分钟前
魔改编译-永久解决selenium痕迹(二)
javascript·python
阿幸软件杂货间29 分钟前
阿幸课堂随机点名
android·开发语言·javascript
一涯31 分钟前
Cursor操作面板改为垂直
前端
我要让全世界知道我很低调38 分钟前
记一次 Vite 下的白屏优化
前端·css
threelab38 分钟前
three案例 Three.js波纹效果演示
开发语言·javascript·ecmascript
1undefined239 分钟前
element中的Table改造成虚拟列表,并封装成hooks
前端·javascript·vue.js
蓝倾1 小时前
淘宝批量获取商品SKU实战案例
前端·后端·api
comelong1 小时前
Docker容器启动postgres端口映射失败问题
前端