事件流及事件委托

前言

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. 选择合适的父元素:选择一个恰当的父元素进行事件委托,通常是包含所有目标元素的最近的父容器。

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

相关推荐
蜗牛快跑213几秒前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy1 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
用户3157476081351 小时前
成为程序员的必经之路” Git “,你学会了吗?
面试·github·全栈
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇1 小时前
ES6进阶知识一
前端·ecmascript·es6
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法