面试官:你了解js事件代理吗?

635 阅读7分钟

前言

在前端开发中,事件处理是非常重要的一部分。随着应用变得越来越复杂,页面上的元素数量也会逐渐增加。如果为每一个元素单独绑定事件监听器,不仅会让代码变得冗长,还会导致性能下降。这时,事件代理(Event Delegation)就成为了一种优化方案。本文将通过几个具体的示例来解释什么是事件代理,并帮助理解其工作原理。

什么是事件代理?

事件代理是一种在父级元素上监听事件,从而间接管理子级元素事件的技术。这种方法利用了事件冒泡的原理:事件会从最深的节点开始逐级向上冒泡,直到达到最顶层的节点。通过在较高级别的元素上监听事件,我们可以捕获发生在任何子元素上的事件,而无需为每个子元素单独绑定事件处理器。

事件代理的好处

  1. 减少事件处理器:只需要为一个父元素绑定事件处理器,而不是为每一个子元素。
  2. 易于维护:当子元素动态增加或减少时,不需要重新绑定事件。
  3. 性能提升:减少事件处理器的数量可以降低内存消耗和提高性能。

事件代理的工作原理

事件代理的关键在于理解事件冒泡机制。当用户点击页面上的某个元素时,事件会首先在这个元素上触发,然后向上冒泡,直到到达文档的根节点。我们可以利用这一点,在较高层级的元素上监听事件,然后通过事件对象来判断实际触发事件的元素。

正文

让我们通过几个具体的例子来更好地理解事件代理的实现方式。

示例一:为列表项单独绑定事件

场景:如果有一个列表,里面有几十个<li>标签,让你点击哪个就显示哪个的内容你会怎么做? 诶,这时我们会想,这还不简单吗,每个都绑定点击事件咯,再把它标签的内容展示出来不就好了,下面代码就是如此:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件代理示例</title>
</head>
<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
         
    <script>
        let lis = document.querySelectorAll('li');
        lis.forEach((li, i) => {
            li.addEventListener('click', function () {
                console.log(this.innerText);
            });
        });
    </script>
</body>
</html>

这里简略点就写了三个<li>,那那么多<li>每个都绑定那不是太麻烦嘛,可不可以简单一点的,诶,还真有,下面为用了事件代理的升级版:

示例二:使用事件代理为列表项绑定事件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件代理示例</title>
</head>
<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
         
    <script>
        let ul=document.querySelector('ul')
        ul.addEventListener('click',function(e){
            console.log(e.target.innerText)
        })
    </script>
</body>
</html>

效果:

02842ef2596ae325081a8c5196256200.gif 可以看到,我们点击不同的<li>一样可以实现我们想要的效果,我们仅在 <ul> 元素上添加了一个点击事件监听器。当用户点击任意一个 <li> 时,事件会冒泡到 <ul>,然后我们在事件处理函数中检查 e.target 是否是我们想要的目标元素,这里是 <li>。如果是,我们就打印出该 <li> 的文本内容。这种方式很好减少了事件处理器的数量,提高了性能。

如果没理解没关系,我们看下面这个例子就明白了。

示例三:多层嵌套元素的事件代理

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #grand{
            width:400px;
            height: 400px;
            background-color: rgba(0, 0, 255, 0.527);
        }
        #parent{
            width: 300px;
            height: 300px;
            background-color: red;
        }
        #child{
            width: 200px;
            height: 200px;
            background-color: yellow;
        }
    </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", function(e){
            console.log("grand");
        })

        parent.addEventListener("click", function(e){
            console.log("parent");
        })

        child.addEventListener("click", function(e){
            console.log("child");
        })

    </script>   
</body>
</html>

c8be72f80f79b965fc530be67b963ed6.gif 在这个示例中,我们有一个三层嵌套的结构。可以看到,当点击 grand元素(蓝色部分)时,打印出了grand;点击parent元素(黄色部分)时,打印出了parent、grand;点击child元素(红色部分)时,打印出了child、parent、grand

要明白原理我们就要了解一下js中的事件流了,js中的事件流分为三个阶段:

  1. 捕获阶段:事件从最外层的元素(如 window 对象)开始向内传播,直到达到目标元素。
  2. 目标阶段:事件在目标元素上触发。
  3. 冒泡阶段:事件从目标元素开始向外传播,直到达到最外层元素。

重点:js中的事件默认在冒泡阶段触发

所有当我们点击child元素(红色部分)时,js就会从window开始捕获事件,到html,再到body,再到grand(有一个点击事件),再到parent(有一个点击事件),最后到目标元素child(有一个点击事件),ok找到之后,开始触发,就会有我们在控制台看到的效果:child,再parent,再grand。所有上面点击<li>标签就会冒泡到<ul>触发事件。

我们知道这个之后,我们再来看看:

        grand.addEventListener("click", function(e){
            console.log("grand");
        },false)

        parent.addEventListener("click", function(e){
            console.log("parent");
        },true)

        child.addEventListener("click", function(e){
            console.log("child");
        },false)

其他代码不变,就加了true,false,false是默认的,不写默认为false表示在冒泡阶段触发,写true表示在捕获阶段就触发,所有当点击child时,打印的是parent、child、grand

再修改来看看:

       grand.addEventListener("click", function(e){
            console.log("grand");
        },false)

        parent.addEventListener("click", function(e){
            e.stopPropagation()    // 点击child部分,打印child、parent、parent1
            // e.stopImmediatePropagation()
            console.log("parent");
        },false)

        parent.addEventListener("click", function(e){
            console.log("parent1");
        })

        child.addEventListener("click", function(e){
            console.log("child");
        },false)
        parent.addEventListener("click", function(e){
            // e.stopPropagation()
            e.stopImmediatePropagation()  // 点击child部分,打印child、parent
            console.log("parent");
        },false)
  • e.stopPropagation(): 阻止事件流的传播,会一直冒泡到这个标签,里面及本身的事件都会触发,外面的都不会触发
  • e.stopImmediatePropagation(): 阻止事件流的传播+阻止同一个容器绑定多个相同的事件

用处: 我们看这个图片:

image.png

我们点击js检测页面空闲这个大的部分是不是会进入到这个页面,那如果我们点击点赞的部分是不是会变得高亮呀,如果我们不设置上面的方法,那么是不是点击点赞部分就会冒泡触发外面大的部分进行跳转呀,所有这个方法还是很重要的。

示例四:使用 onclick 多次绑定事件

<body>
    <div id="grand" onclick="handleGrand()" onclick="handleGrand()">
        <div id="parent" onclick="handleParent()">
            <div id="child">

            </div>
        </div>
    </div>
    <script>
        function handleGrand(){
            console.log("grand1");
        }

        function handleGrand(){
            console.log("grand2");
        }

        function handleParent(){
            console.log("parent");
            
        }
    </script>
    
    // 点击parent部分,打印:parent,grand2
</body>

我们只改变body里面的代码,在这个示例中,我们尝试在 grand 元素上使用 onclick 属性多次绑定事件处理器。然而,由于 onclick 是 DOM0 的方法,只能绑定一个事件处理器,第二个 handleGrand 函数会覆盖第一个,这种方式不如使用 addEventListener 方便和灵活。

示例五:输入框的焦点和失焦事件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" name="" id="input">

    <script>
        let input = document.getElementById('input');

        input.addEventListener('focus', function(e) {
            console.log('focus');
        })

        input.addEventListener('blur', function(e) {
            console.log('blur');
            
        })
    </script>
</body>
</html>

在这个示例中,我们为输入框绑定了焦点focus和失焦blur事件。这两种事件不属于事件冒泡的一部分,因此不能通过事件代理来实现。

总结

js中的事件流:

1.捕获阶段 --- 事件从window处往目标处传播

2.目标阶段 --- 在目标处触发事件

3.冒泡阶段 --- 事件从目标处往window处传播,这个就是冒泡阶段

重点:js 中的事件默认在冒泡阶段触发

  • e.stopPropagation() 阻止事件流的传播
  • e.stopImmediatePropagation() 阻止事件流的传播+阻止同一个容器绑定多个相同的事件

DOM0 vs DOM2

  • DOM0:onclick... 1.无法人为修改事件在哪个阶段触发 2.只能绑定一个相同的事件

  • DOM2:addEventListener 1.可以人为修改事件在哪个阶段触发 2.同一个容器允许绑定多个相同的事件

事件代理: 把元素身上需要响应的事件,委托到另一个元素(父元素)上

无事件件流: 1.focus 2.blur


本文到此就结束了,感谢你的阅读,希望对你有所帮助!