事件流及事件委托

前言

JavaScript 事件流描述的是从页面中接收事件的顺序,这个流程从最外层的对象开始(通常是 window 对象),然后经过特定的元素,直到达到触发事件的目标元素。理解事件流对于理解事件处理机制和实现事件委托是非常重要的。

正文

事件机制是指当特定的操作(如点击按钮、移动鼠标等)在DOM元素上发生时,会触发相应的事件。JavaScript通过监听事件并绑定对应的处理函数来响应用户的操作,对用户的交互做出响应。

事件流有三个阶段:

1. 捕获阶段(Capture Phase)

事件从最外层的对象(通常是 window 对象)开始,然后向下穿过DOM树,直到达到触发事件的目标元素。在捕获阶段,事件是从外向内传播的。

例如:window -> document -> html -> body -> div

2. 目标阶段(Target Phase)

事件已经到达了触发它的目标元素。这是事件处理函数实际执行的阶段。

3. 冒泡阶段(Bubbling Phase)

事件从目标元素开始向上冒泡,经过DOM树上的父元素,直到达到最外层的对象(通常是 window 对象)。在冒泡阶段,事件是从内向外传播的。

例如:div -> body -> html -> document -> window。

这是我从网上找来的一张图,相信大家看完之后可以更好的理解事件捕获和事件冒泡。

好的,相信大家看完事件流的概念之后,对事件流这个机制有一定的理解,接下来我们来看看事件捕获和事件冒泡的例子。这里我们使用addEventListener方法来绑定捕获和冒泡事件。

事件绑定

js 复制代码
element.addEventListener(event, handler, useCapture);

addEventListener有三个参数,我们来介绍一下:

1. event(事件类型)

这是一个字符串,指定要监听的事件类型,如 "click", "mouseover", "keydown" 等。

2. handler(事件处理函数)

这是一个函数,当指定的事件类型在指定的元素上触发时,该函数就会被调用。事件对象(Event 对象)将作为参数传递给处理函数,你可以使用这个对象来获取事件的详细信息,如事件目标、鼠标坐标、键盘按键等。

3. useCapture(是否使用捕获阶段)

这是一个可选的布尔值,决定事件是在捕获阶段(true)还是冒泡阶段(false)进行处理。

  • true:使用捕获阶段进行事件处理。
  • false:使用冒泡阶段进行事件处理。

默认值 :如果不提供 useCapture 参数,它默认为 false,即在冒泡阶段处理事件。

html 复制代码
    <style>
        #app{
            width: 400px;
            height: 400px;
            background-color: aqua;
        }
        #wrap{
            width: 200px;
            height: 200px;
            background-color: blueviolet;
        }
        #box{
            width: 100px;
            height: 100px;
            background-color: #000;
        }
    </style>
    <div id="app">
        <div id="wrap">
            <div id="box"></div>
        </div>
    </div>
    <script>
        let app = document.getElementById('app')
        let wrap = document.getElementById('wrap')
        let box = document.getElementById('box')
        app.addEventListener('click', () => {  
            console.log('app');
        }, true)

        wrap.addEventListener('click', () => {
            console.log('wrap');
        })

        box.addEventListener('click', () => {
            console.log('box');
        })
    </script>
    

我们点击图中黑色方块,也就是box容器,然后输出box, wrap, app,因为我们的addEventListener函数第三个并没有写参数,而它在没有参数的情况下默认是false,也就是说该事件会在冒泡阶段时处理。我们上面提到过,事件从目标元素开始向上冒泡,经过DOM树上的父元素,直到达到最外层的对象,而因为box被包裹在wrap里,wrap被包裹在app里,所以是先输出box,wrap,app.

而如果点击紫色元素,则输出wrap, app。点击蓝色,输出app。

js 复制代码
        app.addEventListener('click', () => {  
            console.log('app');
        }, true)

        wrap.addEventListener('click', () => {
            console.log('wrap');
        })

        box.addEventListener('click', () => {
            console.log('box');
        },true)

这里我们将appbox容器点击事件的第三个参数设置为true,也就是说两个容器的点击事件会在捕获阶段时处理。而wrap的点击事件在冒泡阶段处理。那么我们点击box(黑色容器),将输出app, box, wrap。当我们点击box容器时,先进行捕获阶段,事件从最外层元素开始,逐渐向内部元素传播,所以先打印app,再打印box。当事件到达目标元素时,再发生冒泡事件,从目标元素开始,逐渐向外部元素传播,所以最后打印wrap。

而如果我们将三个点击事件的第三个参数全部设置为true时,那么他们都将在捕获阶段进行处理,则打印app, wrap, box。

阻止事件传播

event.stopPropagation()

当我们调用此方法时,会阻止事件进行传播下去,但不会阻止其它事件处理程序被触发。我通过一个例子来给大家讲解:

js 复制代码
        app.addEventListener('click', (e) => {  // 绑定, 订阅, 注册
            console.log('app');
            e.stopPropagation()
        }, true)

        wrap.addEventListener('click', () => {
            console.log('wrap');
        }, )

        box.addEventListener('click', (e) => {
            console.log('box');
        }, true)

如上述代码,在正常情况下,点击box容器,如果不加这一行代码e.stopPropagation(), 那么appbox在捕获阶段触发,wrap在冒泡阶段触发。但如果在app容器的事件监听函数上面加了这一段代码,那么它会阻止事件进行传播。也就是说,当捕获阶段时,事件传播到了app的容器上,发现addEventListener函数的第三个参数为true,那么则触发该点击事件,但是发现这个点击事件的回调函数内有e.stopPropagation(),它会阻止事件继续传播下去,所以本该继续向内传播的事件被终止。所以这里只打印app

js 复制代码
        app.addEventListener('click', (e) => {  // 绑定, 订阅, 注册
            console.log('app');     
        }, true)

        wrap.addEventListener('click', () => {
            console.log('wrap');
        }, )

        box.addEventListener('click', (e) => {
            console.log('box');
            e.stopPropagation()
        })

来看看这段代码,点击box容器,那么在捕获阶段打印app,在冒泡阶段,当事件传播到box身上时,触发,打印box,但是监听器的回调函数里面存在e.stopPropagation(),所以它会阻止事件继续传播,最终打印 app, box。

调用该方法,不会阻止同一元素其它事件被触发

js 复制代码
        app.addEventListener('click', (e) => {  // 绑定, 订阅, 注册
            console.log('app');     
        }, true)

        wrap.addEventListener('click', () => {
            console.log('wrap');
        }, )

        box.addEventListener('click', (e) => {
            console.log('box');
            e.stopPropagation()
        })
        box.addEventListener('click', (e) => {
            console.log('box2');
        })

如图,打印app, box, box2。该方法不会阻止同一元素其他事件被触发。

event.stopImmediatePropagation()

调用该方法,它的效果跟event.stopPropagation()类似,但是会阻止同一元素的相同事件被触发

js 复制代码
        app.addEventListener('click', (e) => {  // 绑定, 订阅, 注册
            console.log('app');     
        }, true)

        wrap.addEventListener('click', () => {
            console.log('wrap');
        }, )

        box.addEventListener('click', (e) => {
            console.log('box');
            event.stopImmediatePropagation() 
        })
        box.addEventListener('click', (e) => {
            console.log('box2');
        })

若我们在box的监听上加上event.stopImmediatePropagation()函数,那么它会阻止box元素相同事件的触发,例如上述代码中box绑定的第二段点击事件。所以只会输出app, box

事件委托

事件委托(Event Delegation)是一种常用的 JavaScript 设计模式,用于处理事件监听和处理,特别是当需要为大量的子元素添加相同类型的事件监听器时。通过事件委托,我们可以将事件监听器绑定到父元素(通常是包含所有子元素的容器),而不是直接绑定到每一个子元素。

html 复制代码
    <ul id="ul">
        <li>a</li>
        <li>b</li>
        <li>c</li>
        <li>d</li>
        <li>e</li>
    </ul>

如果我们想为每一个列表项添加点击事件,传统的方法可能是遍历每一个列表项并分别绑定事件处理函数:

js 复制代码
    let lis = document.querySelectorAll('li')
    lisforEach((li) => {
        li.addEventListener('click', () => {
            console.log(li.innerText);
        })
    })

虽然这样添加点击事件也是没问题的,但是这只是节点少的情况下,如果节点多的时候,那么就需要造成十分大的性能消耗。

使用事件委托,我们只需将事件监听器绑定到 ul 元素:

js 复制代码
    let ul = document.getElementById('ul')
    ul.addEveListener('click', (e) => {
        // console.log(e);
        console.log(e.target.innerText);
    })

当你点击列表项时,事件会冒泡到ul元素,然后通过检查e.target.innerText,来判断我们点击的是列表项中的哪一个元素。

不管li有多少个,我们最终只需要维护一个函数就够了!

总结一下

注意事项:

  1. 事件冒泡:事件委托依赖于事件冒泡机制,因此确保不阻止事件冒泡或在冒泡阶段处理事件。
  2. 目标检查 :在事件处理函数中,通常需要检查 event.targetevent.currentTarget 来确定是哪一个元素触发了事件。
  3. 选择合适的父元素:选择一个恰当的父元素进行事件委托,通常是包含所有目标元素的最近的父容器。

事件委托是一种强大而灵活的技术,特别适用于处理大量的子元素事件。通过将事件监听器绑定到父元素,我们可以提高性能,简化代码,并支持动态添加的元素,从而更高效地管理和处理事件。

相关推荐
范文杰几秒前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 分钟前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪17 分钟前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy1 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom2 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom2 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom2 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom2 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom2 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试