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

简介: 事件,是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');
});
复制代码

浏览器显示效果如下。

12.png

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


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事件流

1.png

从图中可以看出,元素事件响应在 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 时,浏览器执行结果如下:

22.png

如我们所预想的,冒泡是从下往上冒泡,所以最终的执行顺序为 事件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 时,浏览器执行结果如下:

21.png

如我们所预想的,捕获是从上往下捕获,也就是从外层向里层捕获,所以最终的执行顺序为 事件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 时,浏览器执行结果如下:

20.png

如我们所预想的,先对事件进行捕获,后再对事件进行冒泡。当捕获时,事件从外往内捕获,所以打印结果是冒泡是 捕获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 时,执行结果如下:

15.png


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

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


四、总结和回顾



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

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

  • DOM事件流有3个阶段:捕获阶段,目标阶段,冒泡阶段。三个阶段的顺序为:捕获阶段 → 目标阶段 → 冒泡阶段。
  • 对于目标阶段和非目标阶段的元素,事件响应执行顺序都遵循先捕获后冒泡的原则。
    注:目标阶段即当前所点击事件,即为目标阶段。非目标阶段即外围所影响的事件即为非目标阶段。
  • 事件捕获是从顶层的Window逐层像内层执行,事件冒泡则相反;
  • 事件代理(即事件委托)是根据事件冒泡或事件捕获的机制来实现的。

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

Q1:描述事件冒泡的流程

A1:

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

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

A2:

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


五、结束语



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

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


相关文章
|
8月前
事件代理和事件委托
事件代理和事件委托
60 6
|
8月前
什么是事件代理?什么事件委托?
什么是事件代理?什么事件委托?
53 0
|
8月前
|
JavaScript
事件触发、事件捕获与事件冒泡(js的问题)
事件触发、事件捕获与事件冒泡(js的问题)
47 0
|
2月前
|
JavaScript 前端开发 UED
事件冒泡
【10月更文挑战第29天】事件冒泡是 JavaScript 中一种强大的事件传播机制,理解和正确使用事件冒泡可以帮助开发者实现更加高效、灵活的网页交互效果。在实际开发中,需要根据具体的需求和场景,合理地利用事件冒泡,并注意处理可能出现的事件冲突和兼容性问题。
|
2月前
|
JavaScript 前端开发
事件委托
【10月更文挑战第29天】事件委托是JavaScript中一种重要的事件处理机制,它能够有效地提高事件处理的效率,减少内存占用,尤其适用于处理大量具有相似行为的DOM元素的事件。
|
2月前
|
JavaScript 前端开发 数据安全/隐私保护
事件捕获
【10月更文挑战第29天】事件捕获作为JavaScript中重要的事件传播机制之一,为开发者提供了一种在事件到达目标元素之前进行处理和控制的方式。通过合理地运用事件捕获,可以实现更灵活、更强大的事件处理逻辑,但同时也需要注意其兼容性、事件顺序和性能等方面的问题,以确保事件处理的正确性和高效性。
|
4月前
|
JavaScript 前端开发 API
你真的理解事件绑定、事件冒泡和事件委托吗?
该文章详细解释了JavaScript中的事件绑定机制、事件冒泡行为以及事件委托技术,并通过实例帮助读者更好地理解和应用这些概念。
|
4月前
|
存储 JavaScript 前端开发
js事件冒泡和事件委托
事件冒泡是指事件从最内层元素开始逐级向上传播至祖先元素的过程,默认情况下,点击子元素时会先触发自身的事件处理程序,再依次向上触发父元素的处理程序。事件委托则是在父元素上设置事件处理程序,利用事件冒泡机制处理子元素的事件,以此减少内存消耗和提高性能,特别适用于处理大量动态子元素的情况。其区别在于事件冒泡是事件传播机制的一部分,而事件委托是编程技巧,通过在父元素上绑定事件处理程序来简化子元素的事件处理。
31 0
|
5月前
|
JavaScript 前端开发
绑定事件的方法有几种?
绑定事件的方法有几种?

热门文章

最新文章