• 作者:老汪软件技巧
  • 发表时间:2024-09-02 21:02
  • 浏览量:

事件代理(Event Delegation)是前端开发中一种常用的事件处理技术。它是基于事件冒泡机制的一种优化方案,通常用于处理大量子元素的事件,尤其是当这些子元素是动态生成的时候。简单来说就是原本绑定在子元素上的事件不绑定在子元素上,而是一起交给父元素进行代理触发。

先来个情景,如果一个ul中有多个子元素li,希望点击任意一个li,都能在控制台输出这个li中的内容。

 <ul>
    <li>1li>
    <li>2li>
    <li>3li>
ul>

最容易想到的当然是给每个li分别绑定一个点击事件,但这需要给每个li取一个id来分别进行绑定,这么写既不优雅,并且在有大量li或li是动态生成的情况时,可能会造成大量代码的冗余,导致性能问题和代码难以维护的问题。实际工作中遇到这种情况时,如果不想卷铺盖走人最好不要轻易尝试(doge)。

那么,有没有更好的方法呢?当然是有的,我们直接获取到所有的li形成一个数组,再去遍历这个li数组去给每个li绑定点击事件。

let lis = document.querySelectorAll('li');
lis.forEach((li,i) => {
    li.addEventListener('click',function (){
    console.log(this.innerHTML);
    })
})

但是,我们知道,在js中,函数是存在堆内存中的,如果有n个li,就要在堆中存入n个回调函数,这样太浪费内存,性能不好。

还能怎么优化呢?我们先来了解一下要用到的基础知识。

JS中的事件流机制

JavaScript 中的事件流机制描述了事件是如何在DOM树中传播的。事件流分为三个阶段:捕获阶段、目标阶段和冒泡阶段。

给出三个嵌套的div,分别为grand、parent、child,我们分别给这三个div写点简单的样式。再绑定一个点击事件,输出对应的内容。

html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Documenttitle>
    <style>
        #grand {
            width: 400px;
            height: 400px;
            background-color: #f7dc6f;
        }
        #parent {
            width: 300px;
            height: 300px;
            background-color: #b3d9ff;
        }
        #child {
            width: 200px;
            height: 200px;
            background-color: #d2b4de;
        }
    style>
head>
<body>
    <div id="grand">
        <div id="parent">
            <div id="child">div>
        div>
    div>
    <script>
        let grand = document.getElementById("grand");
        let parent = document.getElementById("parent");
        let child = document.getElementById("child");
        grand.addEventListener("click", () => {
            console.log("grand");
        })
        parent.addEventListener("click", () => {  
            console.log("parent");
        })
        child.addEventListener("click", () => {
            console.log("child");
        })
    script>
body>
html>

点击最小的子元素child,我们观察控制台的输出可以发现,三个元素上的点击事件都被触发了,且顺序为child、parent、grand;同样的,点击parent元素,会触发自身的点击事件和它的父元素grand的点击事件,顺序为parent、grand。点击grand只触发了grand上的点击事件。

为什么点击子元素还能触发父元素身上绑定的事件呢?这时,有两个可能的猜测:1.事件会从子元素传到父元素,2.这里的child元素也在父元素的区域内,点击child也相当于点击了parent。

严谨起见,我们修改一下子元素的大小,为了方便辨识又给子元素加上了标记。

#grand {
    width: 400px;
    height: 400px;
    background-color: #f7dc6f;
}
#parent {
    width: 200px;
    height: 300px;
    background-color: #b3d9ff;
}
#child {
    width: 300px;
    height: 200px;
    background-color: #d2b4de;
}

点击parent范围内的child部分和范围外的child部分,输出的结果依然是一样的,说明点击事件的传播与范围是没有关系的,只和父元素和子元素的关系有关。这个传播机制,就是js中的事件流。

JS中的事件流分为捕获阶段、目标阶段和冒泡阶段

_事件代理是什么原理实现_事件代理使用场景

捕获阶段 --- 事件从window处往目标处传播(此阶段默认并不触发事件)目标阶段 --- 在目标处触发事件冒泡阶段 --- 事件从目标处往window传播(js中的事件默认在冒泡阶段触发)

e.stopPropagation()和e.stopImmediatePropagation()

一栏文章,根据前面提到的事件流,如果我们点击作为子元素的点赞按钮,那么在冒泡阶段应该也势必会触发父元素文章栏的点击事件,跳转到这篇文章详情页。但实际上并没有,我们可以只点赞而不跳转页面。

实现这样的效果,就需要我们阻止冒泡,让事件流在目标阶段触发了目标事件后就结束,不再继续执行冒泡。在事件对象e上,有一个stopPropagation()方法,这个方法可以阻止事件流的继续传播。

grand.addEventListener("click", (e) => {
    console.log("grand"); 
}); 
parent.addEventListener("click", (e) => { 
    console.log("parent");
}); 
child.addEventListener("click", (e) => { 
    console.log("child"); 
    e.stopPropagation()
});

在child的点击事件中调用e.stopPropagation(),就能在目标阶段触发目标事件后,阻止继续冒泡,那么就会只触发child的点击事件而不触发父元素上的事件。

e.stopPropagation()不仅可以阻止事件在冒泡阶段继续传播,还可以阻止事件在捕获阶段进一步传播。

通过设置addEventListener()的第三个参数为true或false,我们可以决定事件在捕获阶段是否触发(默认为false),为true则在捕获阶段触发,为false则在冒泡阶段触发。

// 捕获阶段
grand.addEventListener("click", (e) => {
    console.log("grand (capturing)");
}, true);
parent.addEventListener("click", (e) => {
    console.log("parent (capturing)");
    e.stopPropagation();
}, true);
child.addEventListener("click", (e) => {
    console.log("child (capturing)");
}, true);

点击parent,事件在捕获阶段执行,先执行grand上的点击事件,再传播到parent,所以先输出grand再输出parent;点击child,由于事件流在parent执行完后停止,没有传播到child上,所以也是先输出grand再输出parent。

顺带一提,还有一个和e.stopPropagation()长得很像的方法:e.stopImmediatePropagation(),它的作用是阻止当前阶段(捕获或冒泡)内的其他事件处理程序执行。它不仅可以阻止事件流传播,还能阻止当前元素绑定的其他事件的执行。

 // 捕获阶段
grand.addEventListener("click", (e) => {
    console.log("grand");
}, true);
parent.addEventListener("click", (e) => {
    console.log("parent1");
    e.stopImmediatePropagation(); // 立即停止当前阶段的事件处理
}, true);
child.addEventListener("click", (e) => {
    console.log("child");
}, true);
parent.addEventListener("click", (e) => {
    console.log("parent2");
}, true);

parent元素上不仅绑定了输出"parent1"事件,还绑定了输出"parent2"事件,点击parent,事件流在捕获阶段依次触发输出grand、parent1后,阻止了当前阶段的其他事件处理,因此不会再触发parent上绑定的输出parent2事件了。

特殊地,某些事件类型天生就不支持事件流,这意味着它们不会在DOM结构中传播。例如:

js中的事件代理

回到最初的问题,通过js的事件流,我们就可以把元素身上需要响应的事件,委托给它的父元素(或者祖先元素)处理。就像寝室里每个人的外卖都到了,所有人都去拿太浪费时间了,不如喊寝室长一声义父,让寝室长统一去拿(doge)。

html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    <body>
        <ul id="ul">
            <li>1li>
            <li>2li>
            <li>3li>
        ul>
        <script>
        // 用父元素ul来代理 通过事件参数的target属性来获取点击的元素
        let ul = document.querySelector('#ul');
        ul.addEventListener('click', function (e) {
            console.log(e.target.innerText);
        })
    script>
    body>
html>

这种方法的好处在于,无论子元素有多少个,甚至是在运行时动态添加的子元素,都可以通过父元素上的事件监听器来处理事件。这可以减少事件监听器的数量,提高性能,并且使得代码更加灵活和可维护。