你真的理解事件绑定、事件冒泡和事件委托吗?

1,273 阅读9分钟

这是我参与更文挑战的第10天,活动详情查看:更文挑战

事件,是JS Web API比较重要的一个知识点。我们平常所看到的网页,肯多内容都要用到事件。比如说一个点击、一个下拉、一个滚动,都要用到事件进行操作。

一、事件绑定

1、事件和事件绑定时什么?

事件,就是可以被 js 捕获的人为的操作。那什么是人为的操作呢?比如说鼠标的点击、拖动、缩放网页等等行为,且在这些行为被 js 捕获到以后,就是事件。

举个例子:

比如说我现在要去楼下 舍管阿姨帮我开个门禁,那这个 的操作就是一个事件,就相当于在 js 里喊一个函数去干活。

说完事件,接下来说说事件绑定

什么是 javascript 的事件绑定呢?

用上面那个例子来继续阐述。 这个动作是一个事件,那我要怎么样才能做出 这个动作呢?就需要对我这个动作进行一个绑定。可以通过绑定一个函数,这个函数解决了我怎么喊出来的问题。比如,我要去楼下喊,那这个函数里面就说明了我需要去楼下喊的这个过程。

所以,事件绑定可以理解为,在有一个触发事件的前提下,后面紧跟着一个事件处理函数,这个函数里面包含着所要执行动作的具体过程等,这就是事件绑定。

接下来我们用代码来写一个事件绑定的过程。

function bindEvent(elem, type, fn){
    elem.addEventListener(type, fn);
}

const btn1 = document.getElementById('btn1');
bindEvent( btn1 , 'click', event => {
    console.log(event.target); //event.target为获取触发的元素
    event.preventDefault(); //阻止默认行为
    alert('clicked');
});

浏览器显示效果如下。

事件绑定

大家可以看到,通过点击按钮这个事件,获取到触发的元素,这就是一个事件绑定。

2、事件是如何实现的?

事件基于发布订阅模式,就是在浏览器加载的时候会读取事件相关的代码,但是只有实际等到具体的事件触发的时候才会执行。

比如点击按钮,这是个事件 Event ,而负责处理事件的代码段通常被称为事件处理程序 Event Handler ,也就是「启动对话框的显示」这个动作。

Web 端,我们常见的就是 DOM 事件:

  • DOM0 级事件,直接在 html 元素上绑定 on-event ,比如 onclick ,取消的话, dom.onclick = null同一个事件只能有一个处理程序,后面的会覆盖前面的。
  • DOM2 级事件,通过 addEventListener 注册事件,通过 removeEventListener 来删除事件,一个事件可以有多个事件处理程序,按顺序执行,捕获事件和冒泡事件。
  • DOM3级事件,增加了事件类型,比如 UI 事件,焦点事件,鼠标事件。
    • UI事件,即当用户与界面上的元素交互时触发。
    • 焦点事件,即当用元素获得或失去焦点时触发。
    • 鼠标事件,当用户通过鼠标在页面上执行操作时触发。

二、事件冒泡

1、事件模型

W3C中定义的DOM事件流的发生经历三个阶段:捕获阶段(capturing)、目标阶段(targetin)、冒泡阶段(bubbling)。

  • 冒泡型事件:当你使用事件冒泡时,子级元素先触发,父级元素后触发。
  • 捕获型事件:当你使用事件捕获时,父级元素先触发,子级元素后触发。

2、事件模型解析

我们用 W3C 标准的 DOM 事件流模型图来看事件捕获事件冒泡DOM事件流

W3C事件模型

从图中可以看出,元素事件响应在 DOM 树中是从顶层的Window开始,流向目标元素(2),然后又从目标元素流向顶层的Window。

通常,我们将这种事件流向分为(1)捕获阶段,(2)目标阶段,(3)冒泡阶段。-> 序号对应图中的编号

(1)捕获阶段

捕获阶段是指,事件响应从最外层的Window开始,逐层向内层递进,直到到达具体的事件目标元素,如上图中的(1)。同时在捕获阶段,不会处理响应元素注册的冒泡事件。

(2)目标阶段

目标阶段指触发事件的最底层的元素,如上图中的(2)。

(3)冒泡阶段

冒泡阶段与捕获阶段相反,事件的响应是从最底层开始一层一层往外传递到最外层的Window,即一层一层往上冒,如上图中的(3)。

3、addEventListener语法

现在,我们知道了 DOM 事件流的三个阶段分别是先捕获阶段,然后是目标阶段,最后是冒泡阶段。这也就是我们平常所看到的一些面试题里面说的先捕获后冒泡的原因了。到此,相信大家对 DOM 事件流会有一个清晰的了解。

在实际操作中,我们可以通过 element.addEventListener() 函数来设置一个元素的事件模型,具体设置值可以设置为冒泡事件或捕获事件。

先来看下 addEventListener 函数的基本语法:

element.addEventListener(type, listener, useCapture);

其中,三个参数的含义如下:

type:监听事件类型的字符串;

listener:事件监听的回调函数,即事件触发后要处理的函数;

useCapture:默认值为false,表示事件冒泡;当设置为true时,表示事件捕获。

4、事件冒泡和事件捕获举例

接下来我们用几个实例来运用事件冒泡和事件捕获。

(1)事件冒泡

先附上一段代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    #a{
        background-color: darkcyan;
        line-height: 40px;
        color: cornsilk;
    }
    #b{
        background-color: chocolate;
    }
    #c{
        background-color: cornflowerblue;
    }
</style>
<body>
    
    <div id="a">
        事件a
        <div id="b">
            事件b
            <div id="c">
                事件c
            </div>
        </div>
    </div>

    <script>
        let a = document.getElementById('a');
        let b = document.getElementById('b');
        let c = document.getElementById('c');

        //注册冒泡事件监听器
        a.addEventListener('click', () => {console.log("冒泡a")});
        b.addEventListener('click', () => {console.log('冒泡b')});
        c.addEventListener('click', () => {console.log("冒泡c")});
    </script>
</body>
</html>

当我们点击 事件c 时,浏览器执行结果如下:

事件冒泡举例

如我们所预想的,冒泡是从下往上冒泡,所以最终的执行顺序为 事件c → 事件b → 事件a ,打印出 冒泡c → 冒泡b → 冒泡a

(2)事件捕获

先附上一段代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    #a{
        background-color: darkcyan;
        line-height: 40px;
        color: cornsilk;
    }
    #b{
        background-color: chocolate;
    }
    #c{
        background-color: cornflowerblue;
    }
</style>
<body>
    
    <div id="a">
        事件a
        <div id="b">
            事件b
            <div id="c">
                事件c
            </div>
        </div>
    </div>

    <script>
        let a = document.getElementById('a');
        let b = document.getElementById('b');
        let c = document.getElementById('c');

        //注册捕获事件监听器
        a.addEventListener('click', () => {console.log("捕获a")}, true);
        b.addEventListener('click', () => {console.log('捕获b')}, true);
        c.addEventListener('click', () => {console.log("捕获c")}, true);
    </script>
</body>
</html>

此时,我们给 addEventListener 加上 true 的属性,因此,当我们点击 事件c 时,浏览器执行结果如下:

事件捕获举例

如我们所预想的,捕获是从上往下捕获,也就是从外层向里层捕获,所以最终的执行顺序为 事件a → 事件b → 事件c ,打印出 捕获a → 捕获b → 捕获c

(3)事件捕获VS事件冒泡

接下来,我们将上述的代码 事件abc 三个元素都注册上捕获和冒泡事件,并以 事件c 作为触发事件的主体,即事件c为事件流中的目标阶段。

附上一段代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    #a{
        background-color: darkcyan;
        line-height: 40px;
        color: cornsilk;
    }
    #b{
        background-color: chocolate;
    }
    #c{
        background-color: cornflowerblue;
    }
</style>
<body>
    
    <div id="a">
        事件a
        <div id="b">
            事件b
            <div id="c">
                事件c
            </div>
        </div>
    </div>

    <script>
        let a = document.getElementById('a');
        let b = document.getElementById('b');
        let c = document.getElementById('c');

        //注册冒泡事件监听器
        a.addEventListener('click', () => {console.log("冒泡a")});
        b.addEventListener('click', () => {console.log('冒泡b')});
        c.addEventListener('click', () => {console.log("冒泡c")});
        //注册捕获事件监听器
        a.addEventListener('click', () => {console.log("捕获a")}, true);
        b.addEventListener('click', () => {console.log('捕获b')}, true);
        c.addEventListener('click', () => {console.log("捕获c")}, true);
    </script>
</body>
</html>

当我们点击 事件c 时,浏览器执行结果如下:

事件冒泡vs事件捕获

如我们所预想的,先对事件进行捕获,后再对事件进行冒泡。当捕获时,事件从外往内捕获,所以打印结果是冒泡是 捕获a → 捕获b → 捕获c当冒泡时,事件由内往外冒泡,所以最终的打印结果为 冒泡c → 冒泡b → 冒泡a

三、事件代理(事件委托)

讲完事件冒泡和事件代理,那么对于事件代理就比较容易理解了。

事件代理,即事件委托。事件代理就是利用事件冒泡或者事件捕获的机制把一系列的内层元素事件绑定到外层元素上。

我们来看个例子。

<ul id="item-list">
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
    <li>item 4</li>
</ul>

比如说,我们要给这个 ul 列表下面的每个 li 元素绑定事件。如果按照传统方法处理的话,我们可能会一个一个去绑定。数据量小的时候可能还好,但如果遇到数据量大的时候呢?一个一个绑定也太可怕了。

因此就有了事件代理。我们可以通过使用事件代理,将绑定多个事件的操作变为只绑定一次的操作,这样就极大减少了代码的重复编写。

因此,利用事件冒泡或事件捕获,来达到事件代理的效果。具体实现方式如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   <ul id="item-list">
        <li>item 1</li>
        <li>item 2</li>
        <li>item 3</li>
        <li>item 4</li>
    </ul>
    <script>
        let items = document.getElementById('item-list');
        //通过事件冒泡实现事件代理
        items.addEventListener('click', (e) => {console.log('冒泡:click ',e.target.innerHTML)}, false);
        //通过事件捕获实现事件代理
        items.addEventListener('click', (e) => {console.log('捕获:click ',e.target.innerHTML)}, true);
    </script>
</body>
</html>

当点击列表中的 item 时,执行结果如下:

事件代理

从上图中可以看出,当点击目标元素时,可以对其进行捕获,在捕获结束后,对其进行冒泡操作,且达到了点击当前元素只显示当前元素的效果。

同时,细心的小伙伴已经发现,在我们上面的代码中,编写顺序是先冒泡后捕获。但结果打印依然是先捕获后冒泡。这也就顺应了我们上面所说的,关于DOM事件流的顺序,都是先捕获后冒泡,而跟实际的代码顺序是没有关系的。

四、总结和回顾

讲完事件绑定、DOM事件流模型中的事件冒泡和事件捕获以及事件代理,我们来做个总结和回顾。

(1)以上的内容总结下来有以下几点:

  • DOM事件流有3个阶段:捕获阶段,目标阶段,冒泡阶段。三个阶段的顺序为:捕获阶段 → 目标阶段 → 冒泡阶段。

  • 对于目标阶段和非目标阶段的元素,事件响应执行顺序都遵循先捕获后冒泡的原则。

    注:目标阶段即当前所点击事件,即为目标阶段。非目标阶段即外围所影响的事件即为非目标阶段。

  • 事件捕获是从顶层的Window逐层像内层执行,事件冒泡则相反;

  • 事件代理(即事件委托)是根据事件冒泡或事件捕获的机制来实现的。

(2)用几个题目来回顾下我们上面所讲的知识点

Q1:描述事件冒泡的流程

A1:

  • 基于DOM树形结构
  • 事件会顺着所触发的元素,一层一层的往上冒
  • 应用场景:事件代理

Q2:当无限下拉图片列表时,如何监听每个图片的点击?

A2:

  • 用事件代理处理
  • 用e.target获取触发元素
  • 用matches来判断是否触发元素

五、结束语

一直都不是特别清楚为什么是事件是先捕获后冒泡,脑子里也没有个大概框架,文绉绉的文字也不能让我对它有所理解。直到看到了 W3C 的那张 DOM 事件流模型的图,一下子明白了事件为什么是先捕获后冒泡了。因为 Window 对象是直接面向用户的,那么当用户触发一个事件时,如点击事件,肯定时从 Window 对象开始的,然后再向内逐层递进。所以自然也就是先捕获后冒泡了!

关于Web API中的事件就讲到这里啦!如有疑问欢迎评论区留言或私信我交流~

  • 关注公众号 星期一研究室 ,不定期分享学习干货,更多有趣的专栏待你解锁~

  • 如果这篇文章对你有用,记得点个赞加个关注再走哦~