原生DOM中的事件(事件流、事件委托)

861 阅读8分钟

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

大家好,我是一碗周,一个不想被喝(内卷)的前端。如果写的文章有幸可以得到你的青睐,万分有幸~

这是【从头学前端】系列文章的第四十四篇-《原生DOM中的事件》

本系列文章在掘金首发,编写不易转载请获得允许

写在前面

事件是什么

所谓事件,就是浏览器告知JavaScript程序用户的行为,并且会提供一个自动加载某种动作(例如:运行一些代码)的机制。例如:如果用户在网页上点击一个按钮,显示一个信息框来响应这个动作。

在HTML中,时间在浏览器窗口中被触发并且通常被绑定到窗口内部的待定部分——可能是一个元素、一系列元素、被加载到这个窗口的HTML代码或者整个浏览器窗口。

每个可用的事件都会有一个事件处理器,也就是事件触发时会运行的代码块。当我们定义了一个用来回应事件被激发的代码块的时候,我们说我们注册了一个事件处理器。注意事件处理器有时候被叫做事件监听器——从我们的用意来看这两个名字是相同的,尽管严格地来说这块代码既监听也处理事件。监听器留意事件是否发生,然后处理器就是对事件发生做出的回应。

我们常见的事件类型都是DOM预定义好的,也就是说我们可以直接使用的。

事件类型

根据事件的使用场景不同,我们人为的可以将事件分为如下几种

  • 依赖于设备的输入事件:键盘事件和鼠标事件,这些事件都是直接和设备相关的。
  • 独立于设备的输入事件:例如click事件等,这些事件并没有直接与设备相关。
  • 用户界面的相关事件:用户界面事件属于比较高级的事件,一般多用于表单中的组件。
  • 状态变化的相关事件:这些事件与用户行为无关,而是有网络或浏览器触发的。
  • 特定API事件:这些事件多用于特定场景的事件,例如HTML5中提供的拖放API中的事件等。
  • 错误处理的相关事件:一般都是与错误处理有关的事件。

事件的实现步骤

我们事件的实现步骤如下:

  1. 定位页面中指定的元素
  2. 为指定元素绑定事件,具有一个事件监听器(监听用户相应的行为)
  3. 为绑定的事件编写相应的处理函数(用户触发相应的行为时,函数做出的响应)

示例代码如下所示:

<body>
  <button id="btn">按钮</button>
  <script>
    // 1. 定位页面中指定的元素
    var btn = document.getElementById('btn')
    // 2. 为指定元素绑定事件 - 具有一个事件监听器(监听用户相应的行为)
    btn.onclick = function () {
      // 3. 为绑定的事件编写相应的处理函数(用户触发相应的行为时,函数做出的响应)
      console.log("我被点击了")
    }
  </script>
</body>

注册事件

所谓主词事件,就是将JavaScript函数与指定的事件相关联,被绑定的函数成为该事件的句柄。当该事件被触发时,绑定的函数会被调用。

注册事件具有以下三种方式实现:

  • HTML页面元素提供的事件属性
  • DOM标准规范中HTML相关对象提供的事件属性
  • 通过向HTML页面中指定元素添加事件监听器

下面我们就分别说说这三种事件。

HTML页面元素提供的事件属性

HTML页面元素提供的事件属性是元素分类的一种,DOM提供了事件的名称。示例代码如下所示:

<body>
  <button onclick="myClick()"
          id="btn">按钮</button>
  <script>
    function myClick() {
      console.log('你终于点中了我...')
    }
  </script>
</body>

当点击点击页面的按钮后,在console面板就会打印你终于点中了我...

DOM对象提供的事件属性

通过DOM标准规范中的Document对象定位HTML页面的元素,所返回的D0M对象提供了一系列的事件属性,通过这些事件属性可以实现注册事件的功能。示例代码如下:

<body>
  <button id="btn">按钮</button>
  <script>
    var btn = document.getElementById('btn')
    // DOM对象的事件属性
    btn.onclick = myClick;
    function myClick() {
      console.log('你终于点中了我...')
    }
  </script>
</body>

事件监听器

DOM 标准规范中提供了EventTarget.addEventListener()方法,调用该方法表示想指定元素添加事件监视器。语法结构如下

element.addEventListener(eventName, functionName, capture)

参数说明

  • evenName:为元素指定具体的事件名称(注意,没有on)
  • functionName:表示事件的处理函数
  • capture:可选,是否阻止事件冒泡,默认值为false

示例代码如下

<body>
  <button id="btn">按钮</button>
  <script>
    var btn = document.getElementById('btn')
    // 事件监听器
    /*
      element.addEventListener(eventName, functionName)
      * 参数
        * eventName:绑定事件的事件名称(注意,没有on)
        * functionName:表示事件的处理函数
    */
    btn.addEventListener('click', myClick)
    function myClick() {
      console.log('你终于点中了我...')
    }
  </script>
</body>

使用该方法注册事件的时候,使用this就指代当前注册事件的元素。

事件监听器与DOM对象提供的事件属性的区别

事件监听器 与DOM对象提供的事件属性都做到了结构与行为相分离,但是两者是存在着区别的,示例代码如下:

<!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>注册事件的区别</title>
</head>

<body>
  <button id="btn1">按钮1</button>
  <button id="btn2">按钮2</button>
  <script>
    var btn1 = document.getElementById('btn1')
    var btn2 = document.getElementById('btn2')
    // 使用事件监听器为按钮1新增两个事件
    btn1.addEventListener('click', myClick1)

    // 使用DOM对象的事件属性为按钮2绑定两个事件
    btn2.onclick = myClick1
    btn2.onclick = myClick2

    // 执行函数
    function myClick1() {
      console.log('你终于点中了我...')
    }
    btn1.addEventListener('click', myClick2)
    function myClick2() {
      console.log('你终于又点中了我...')
    }
  </script>
</body>

</html>

我们可以看出,使用事件监听器可以绑定多个事件,但是使用 DOM 对象提供的事件属性只可以添加一个(添加第二个时是重新赋值),所以我们一般在实际开发中使用事件监听器的方式。

移除注册事件

EventTarget.removeEventListener()方法删除使用EventTarget.addEventListener()方法添加的事件。

语法结构如下所示:

element.removeEventListener(eventName, functionName, capture)

参数说明

  • evenName:为元素指定具体移除的事件名称(注意,没有on)
  • functionName:表示要移除事件中的某个的处理函数,
  • capture:可选,是否阻止事件冒泡,默认值为false

示例代码如下:

var btn = document.getElementById('btn')
// 使用事件监听器为按钮新增两个事件
btn.addEventListener('click', myClick)

// 执行函数
function myClick() {
  console.log('你终于点中了我...')
}
// 移除 myClick2 这个执行函数
btn.removeEventListener('click', myClick)

Event接口

Event接口表示在DOM中发生的任何事件; 一些是用户生成的(例如鼠标或键盘事件),而其他由 API 生成(例如指示动画已经完成运行的事件,视频已被暂停等等)。

事件处理函数可以附加在各种对象上,包括DOM元素,window对象等。当事件发生时, event对象就会被创建并依次传递给事件监听器。

在处理函数中,将event对象作为第一个参数参数,可以访问DOM Event接口。如下代码展示event对象如何使用:

<body>
  <button id="btn">按钮</button>
  <script>
    var btn = document.getElementById('btn')
    // event 作为第一个参数出现
    btn.addEventListener('click', function (event) {
      console.log(event)
    })
  </script>
</body>

阻止默认行为

默认行为是什么

所谓默认行为,就是指HTML元素不借助JavaScript逻辑原本具有的动态效果。例如以下HTML元素:

  • <a>元素的跳转功能
  • <form>元素中点击<input type="submit">提交按钮是,提交表单的功能。
  • 输入框的输入文本内容。
  • 单选框或者复选框的切换选择功能。

preventDefault()方法

Event事件对象提供了preventDefault()方法,用于阻止浏览器的默认行为。语法结构如下:

event.preventDefault();

示例代码如下:

<body>
  <a href="www.baidu.com">打开百度</a>
  <div id="output"></div>
  <script>
    // 定位a元素
    var a = document.getElementsByTagName('a')[0]
    // 为a绑定点击事件
    a.addEventListener('click', function (event) {
      event.preventDefault()
      var output = document.getElementById('output');
      output.innerHTML += "对不起,<code> event.preventDefault() </code>方法使得 a 元素不可用<br>"

    })
  </script>
</body>

事件流

事件流是什么

所谓事件流,就是当触发某个元素的事件时,事件会按照DOM结构树进行传播,传播的过程如下:

  1. 捕获阶段:该阶段是由网景公司提出的。按照DOM结构由document对象向下的顺序传播,直到目标元素为止。
  2. 目标阶段:该阶段就是值目标元素触发当前事件
  3. 冒泡阶段:该阶段是由微软公司提出的,按照DOM结构树由目标元素向上的顺序传播,直到document对象

触发事件流的条件如下所示:

  • 这些元素之间的关系是祖先与后代的关系
  • 这些元素绑定相同的事件

事件的三个阶段

现在我们通过这段代码来看一下事件的这三个阶段,代码如下:

<!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>事件流</title>
  <style>
    * {
      margin: 0;
      box-sizing: border-box;
    }

    #main {
      height: 300px;
      width: 300px;
      background-color: lightsalmon;
      padding: 50px;
      margin: 100px;
    }

    #div {
      height: 200px;
      width: 200px;
      background-color: lightskyblue;
      padding: 50px;
    }

    #p {
      height: 100px;
      width: 100px;
      background-color: lightyellow;
    }
  </style>
</head>

<body>
  <main id="main">
    <div id="div">
      <p id="p"></p>
    </div>
  </main>
  <script>
    var main = document.getElementById('main')
    var div = document.getElementById('div')
    var p = document.getElementById('p')

    // 绑定相同事件
    main.addEventListener('click', function () {
      console.log('我是main标签')
    }, false)
    div.addEventListener('click', function () {
      console.log('我是div标签')
    }, false)
    p.addEventListener('click', function () {
      console.log('我是p标签')
    }, false)
  </script>
</body>

</html>

该代码的执行结果如下:

事件流.gif

我们可以将这个过程画出如下过程图:

事件执行流程.png

当我们点击<p>这个块的时候,事件会从Window对象开始逐层传递,直到<p>触发click事件,然后执行我们的回调函数;然后执行完毕之后会逐层向上冒泡,然后就会执行为<div><main>绑定的事件处理函数。

取消事件冒泡

取消事件冒泡有两种方式,一种是通过addEventListener()方法注册事件时,将第三个参数设置为true;另一种就是通过event对象提供的stopPropagation()方法。

事件委托

当为大量的HTML元素注册相同事件,并且事件的句柄逻辑完全相同,会造成页面速度下降。不过,事件流允许这些HTML元素的共同父级注册事件。这种方式称为事件委托。

我们先看一段代码

<body>
  <div id="container">
    <button id="btn1">按钮</button>
    <button id="btn2">按钮</button>
    <button id="btn3">按钮</button>
  </div>
  <script>
    var btn1 = document.getElementById('btn1')
    btn1.addEventListener('click', function () {
      console.log('我是按钮')
    })
    var btn2 = document.getElementById('btn2')
    btn2.addEventListener('click', function () {
      console.log('我是按钮')
    })
    var btn3 = document.getElementById('btn3')
    btn3.addEventListener('click', function () {
      console.log('我是按钮')
    })
  </script>
</body>

现在我们通过事件委托来改写这个代码,代码如下

var container = document.getElementById('container')
container.addEventListener('click', function (event) {
  var target = event.target
  // 当前点击的为 button 时,才会触发下面逻辑代码
  if (target.nodeName == "BUTTON") {
    console.log('我是按钮')
  }
})

我们可以明显看到,代码量的节省,但是最终的运行结果是一样的。

写在最后

你如果看到这里,我感到很荣幸,如果你喜欢这篇文章,你可以为这篇文章点上一个小赞;你如果喜欢这个专栏,我会一直更新到百篇以上,可以点一下后面的链接从头学前端 - 一碗周的专栏 - 掘金 (juejin.cn)进入之后给个关注。

最后也可以给我点个关注,万分荣庆。

往期推荐