使用浏览器事件

版权声明:如有转载请注明出处 http://blog.csdn.net/modurookie ,谢谢 https://blog.csdn.net/MoDuRooKie/article/details/82352866

上篇文章学习了如何添加、删除页面内容,以及为页面内容设置样式。我们需要在 JS 文件中编写 JS 代码。但是如果我们在 JS 文件中编写所有代码,当我们加载页面时,所有更改将立即执行。这篇文章将学习如何根据用户的操作,运行操纵 DOM 的 JS 代码

接下来我们将学习:

  • 事件,什么是事件
  • 回应事件,如何监听事件并在事件发生时做出回应
  • 事件数据,掌握事件中所包含的数据
  • 停止事件,防止事件触发多重反应
  • 事件生命周期,事件的生命周期阶段
  • DOM 就绪状态,事件可以知道 DOM 何时准备就绪,可以与之进行交互

简介

查看事件

event = 通知

monitorEvents() 函数会持续吐出发生在目标元素上的所有事件,直到时间终结…或者直到你刷新页面。另外,Chrome 浏览器也有提供一个 unmonitorEvents() 函数,它可以关闭目标元素的事件通知:

// 开始显示 document 对象上的所有事件
monitorEvents(document);

// 关闭 document 对象上所有事件的显示。
unmonitorEvents(document);

monitorEvents 只适用于开发/测试用途,而不应该用于生产代码。请查看 Chrome DevTools 网站上的文档:monitorEvents 文档

回应事件

事件目标

你还记得第一课所讲的节点接口和元素接口吗?你还记得,元素接口是节点接口的子代,因此继承了节点的所有属性和方法吗?

其实,有一点我当时完全跳过了,留到现在才讲。那就是,节点接口继承自 EventTarget 接口

这里写图片描述

所有节点和元素均继承自 EventTarget 接口

根据 EventTarget 页面 的解释,EventTarget 是一个由可以接收事件的对象实现的接口,并且可以为它们创建监听器。元素、文档和窗口是最常见的事件目标。

从上图中可以看出,EventTarget 位于整个链条顶端。也就是说,它不会从任何其他接口继承任何属性或方法。相反,所有其他接口都继承自它,因此包含它的属性和方法。这意味着,以下每一项都是“事件目标”:

  • document 对象
  • 段落元素
  • 视频元素
  • 等等

EventTarget 接口没有任何属性,而只有三个方法!这些方法是:

  • .addEventListener()
  • .removeEventListener()
  • .dispatchEvent()

添加事件监听器

<event-target>.addEventListener(<event-to-listen-for>, <function-to-run-when-an-event-happens>);

可见,事件监听器需要三个要素:

  • 事件目标 - 称为目标(例如 document 对象、<p> 元素等)
  • 要监听的事件类型 - 称为类型(点击、双击、按下的键盘、上的按键、滚动鼠标滚轮、提交表单等等)
  • 事件发生时运行的函数 - 称为监听器
// 目标是页面上的第一个 <h1> 元素
const mainHeading = document.querySelector('h1');
/**
* 要监听的事件类型是 “click” 事件
* 监听器是一个将 “The heading was clicked!” 记录到控制台的函数
*/
mainHeading.addEventListener('click', function () {
  console.log('The heading was clicked!');
});

请查看相关文档,以了解更多信息:addEventListener 文档

向项目中添加事件监听器

在浏览器的开发者工具中运行代码对于测试来说非常有用,但是这个事件监听器只会持续到页面被刷新。与我们希望发送给用户的所有真实的 JavaScript 代码一样,我们的事件监听器代码也需要位于 JavaScript 文件中。

示例:

<html>
    ...
    <body>
        ...
        <script src="app.js"></script>
    </body>
</html>
// app.js
document.addEventListener('click', function() {
    const mainHeading = document.querySelector('h1');
    mainHeading.style.backgroundColor = 'red';
});

要查看所有可以监听的事件的完整列表,请参见事件文档:事件列表。该列表列出了所有可以发生的不同的 DOM 事件,它们按照常见类别进行分类。

移除事件监听器

JS中的对象相等性

相等性是大多数编程语言中的一个常见任务,但在 JavaScript 中,这可能有点棘手,因为 JavaScript 会进行所谓的“强制类型转换”,即尝试将所比较的项目转换为相同的类型(例如字符串、数字)。JavaScript 既有允许进行强制类型转换 的双等号 (==) 运算符,也有防止在比较时进行强制类型转换的三等号 (===) 符号。

对象相等性,它包括对象、数组和函数

{ name: 'Richard' } === { name: 'Richard' }
// 相等性测试结果是 false,两个对象并不相等!

虽然两个对象看起来完全一样,但是也不表示它们完全相同,相同的信息并不表示就完全相同。在使用 JS 对象和处理对等性时,我们需要思考它们是两个不同的对象吗?或者是引用同一对象的两个不同名称吗?

var a = {
    myFunction: function quiz() { console.log('hi'); }
};
var b = {
    myFunction: function quiz() { console.log('hi'); }
};
// 相等性测试结果是 false,这两个 myFunction 函数是不同的函数。它们看起来一样,但却是不同的实体。
function quiz() { ... }

var a = {
    myFunction: quiz
};
var b = {
    myFunction: quiz
}
// 相等性测试结果是 true,这两个 myFunction 函数均指向同一个函数,也就是 quiz 函数。

为什么我们要关心对象/函数的平等性呢?原因在于,.removeEventListener() 方法要求我们向其传递与传递给 .addEventListener() 的函数完全相同的监听器函数

<event-target>.removeEventListener(<event-to-listen-for>, <function-to-remove>);

可见,事件监听器需要三个要素:

  • 事件目标 - 称为目标
  • 要监听的事件类型 - 称为类型
  • 要移除的函数 - 称为监听器

请记住,监听器 函数必须是与 .addEventListener() 调用中使用的函数完全 相同的函数,而不仅仅是一个看起来相同的函数。

function myEventListeningFunction() {
    console.log('howdy');
}

// 为 点击 事件添加一个监听器,来运行 `myEventListeningFunction` 函数
document.addEventListener('click', myEventListeningFunction);

// 立即移除 应该运行`myEventListeningFunction`函数的 点击 事件监听器
document.removeEventListener('click', myEventListeningFunction);
  • 具有相同的目标
  • 具有相同的类型
  • 并传递完全相同的监听器

不具备相等性时,即监听器函数并未指向完全相同的函数。它不会移除监听器:

// 为 点击 事件添加一个监听器,来运行 `myEventListeningFunction` 函数
document.addEventListener('click', function myEventListeningFunction() {
    console.log('howdy');
});

// 立即移除 应该运行`myEventListeningFunction`函数的 点击 事件监听器
document.removeEventListener('click', function myEventListeningFunction() {
    console.log('howdy');
});

MDN 上的 removeEventListener

事件的阶段

事件的生命周期包括三个不同的阶段,分别是:

  • 捕获
  • 目标
  • 冒泡

而且,它们按照以上顺序发生;首先是 捕获 ,其次是 目标 ,再次是 冒泡 阶段。

大多数事件处理器都在目标阶段运行,例如当你将点击事件处理器附加到按钮时。事件到达按钮(即其目标),而在那里只有一个处理器,因此事件处理器得以运行。

<html>
<body>
    <div>
        <p>
            <button>Dare to click me?</button>
        </p>
    </div>
</body>
</html>

点击事件会启动整个流程,首先从捕获阶段开始,它从 HTML 元素开始,一直往下,抵达点击的元素 <button> 后,它会切换到添加目标阶段,然后切换到冒泡阶段并一直往上执行。


document.addEventListener('click', function () {
   console.log('The document was clicked');
});

默认情况下,当仅使用两个参数来调用 .addEventListener() 时,该方法会默认使用冒泡阶段

document.addEventListener('click', function () {
   console.log('The document was clicked');
}, true);

但是用三个参数来调用,第三个参数为 true(意思是它应该在捕获阶段提早激活监听器)。

不同事件阶段的事件处理程序设置过程说明:

这次对段落设置捕获事件监听器,对主体设置冒泡监听器并对按钮设置冒泡监听器,关键区别是段落设为捕获阶段,而主体和按钮默认为冒泡阶段。

这里写图片描述

当按钮被点击时,流程从顶部开始并往下执行,抵达主体元素后,它不会运行该函数,因为我们依然处于捕获阶段。

这里写图片描述

但当它抵达段落部分时,将运行监听器函数。这是因为这个段落设置为了在捕获阶段运行。

这里写图片描述

然后转到按钮部分,从捕获阶段切换到了目标冒泡阶段,然后触发监听器。因为按钮使用了默认设置,即在冒泡阶段运行函数。

这里写图片描述

然后沿着 HTML 步骤往回执行。抵达主体时,运行监听器函数。然后转到 HTML 元素并结束。

这里写图片描述

事件对象

当事件发生时,浏览器包含一个事件对象。这只是一个常规的 JavaScript 对象,包含大量有关事件本身的信息。根据 MDN,.addEventListener() 的监听器函数,在发生指定类型的事件时,会收到一个通知(一个实现事件接口的对象)。

document.addEventListener('click', function (event) {  // ← 全新的 `event` 参数!
   console.log(event);
});

现在,当监听器函数被调用时,它就可以存储传递给它的事件数据了!

这里写图片描述

默认操作

事件对象存储了大量信息,我们可以使用这些数据来做各种事情。不过,专业人员经常使用事件对象来做的一件事,就是阻止默认操作的发生

如果没有事件对象,我们就只能任由默认操作发生。不过,事件对象上有一个 .preventDefault() 方法,处理器可以调用该方法来阻止默认操作发生!

const links = document.querySelector('a');
const thirdLink = links[2];

thirdLink.addEventListener('click', function (event) {
    event.preventDefault();
    console.log("Look, ma! We didn't navigate to a new page!");
});

MDN 上的事件

避免太多事件

重构事件监听器的数量

var myCustomDiv = document.createElement('div');

for (var i = 1; i <= 200; i++) {
    var newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    newElement.addEventListener('click', function respondToTheClick() {
        console.log('A paragraph was clicked.');
    });

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

这里写图片描述

var myCustomDiv = document.createElement('div');

function respondToTheClick() {
    console.log('A paragraph was clicked.');
}

for (var i = 1; i <= 200; i++) {
    var newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    newElement.addEventListener('click', respondToTheClick);

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

这里写图片描述

var myCustomDiv = document.createElement('div');

function respondToTheClick() {
    console.log('A paragraph was clicked.');
}

for (var i = 1; i <= 200; i++) {
    var newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    myCustomDiv.appendChild(newElement);
}

myCustomDiv.addEventListener('click', respondToTheClick);

document.body.appendChild(myCustomDiv);

这里写图片描述

现在只有:

  • 一个事件监听器
  • 一个监听器函数

现在,浏览器无需在内存中存储两百个不同的事件监听器和两百个不同的监听器函数。这大大提高了性能!

但是,如果你测试以上代码,就会注意到我们失去了对单个段落的访问权限。我们无法将特定的段落元素作为目标。那么,我们如何将这个高效的代码与先前访问单个段落项目的能力结合起来呢?

事件代理

事件对象有一个 .target 属性。该属性引用了事件的目标。还记得捕获、目标和冒泡阶段吗?…它们现在也会派上用场!

假设你点击了一个段落元素。整个过程大致如下:

  • 段落元素被点击
  • 事件经历捕获阶段
  • 事件达到目标
  • 事件切换到冒泡阶段,并开始向上爬升 DOM 树
  • 当它碰到 <div> 元素时,就会运行监听器函数
  • 在监听器函数中,event.target 是被点击的元素

因此,event.target 让我们可以直接访问被点击的段落元素。由于我们可以直接访问该元素,因此我们可以访问它的 .textContent修改它的样式更新它所拥有的类——我们可以对它进行任何操作!

var myCustomDiv = document.createElement('div');

function respondToTheClick(evt) {
    console.log('A paragraph was clicked: ' + evt.target.textContent);
}

for (var i = 1; i <= 200; i++) {
    var newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

myCustomDiv.addEventListener('click', respondToTheClick);

检查事件代理中的节点类型

如果我们具有以下 HTML,会发生什么情况:

<article id="content">
  <p>Brownie lollipop <span>carrot cake</span> gummies lemon drops sweet roll dessert tiramisu. Pudding muffin <span>cotton candy</span> croissant fruitcake tootsie roll. Jelly jujubes brownie. Marshmallow jujubes topping sugar plum jelly jujubes chocolate.</p>

  <p>Tart bonbon soufflé gummi bears. Donut marshmallow <span>gingerbread cupcake</span> macaroon jujubes muffin. Soufflé candy caramels tootsie roll powder sweet roll brownie <span>apple pie</span> gummies. Fruitcake danish chocolate tootsie roll macaroon.</p>
</article>
document.querySelector('#content').addEventListener('click', function (evt) {
    console.log('A span was clicked with text ' + evt.target.textContent);
});

这样做是可以的,但有一个重要缺陷。当任何一个段落元素被点击时,监听器函数仍会触发!换句话说,这个监听器函数并没有验证事件目标是否确实是一个 <span> 元素。让我们将这个检查添加上去:

document.querySelector('#content').addEventListener('click', function (evt) {
    if (evt.nodeName === 'SPAN') {  // ← 验证目标是我们需要的元素
        console.log('A span was clicked with text ' + evt.target.textContent);
    }
});

每个元素都从节点接口继承属性。从节点接口继承的属性之一就是 .nodeName。我们可以使用这个属性来验证目标元素确实是我们正在查找的元素。当一个 <span> 元素被点击时,它将有一个 .nodeName 属性为“SPAN”,因此检查将通过,并且该消息将会被记录。但是,如果一个 <p> 元素被点击,它将有一个 .nodeName 属性为“P”,因此检查将失败,并且该消息将不会被记录。

注意,.nodeName 属性将返回一个大写字符串,而不是一个小写字符串。因此,当你执行检查时,请确保,检查大写字母,或者将 .nodeName 转换为小写。

// 用大写字母检查
if (evt.nodeName === 'SPAN') {
    console.log('A span was clicked with text ' + evt.target.textContent);
}

// 将 nodeName 转换为小写
if (evt.nodeName.toLowerCase() === 'span') {
    console.log('A span was clicked with text ' + evt.target.textContent);
}

DOM 何时准备就绪

DOM 是增量式构建的

当 HTML 被接收、转换为令牌并构建文档对象模型时,这是一个连续的过程。当解析器到达一个 <script> 标签时,它必须等待下载脚本文件并执行该 JavaScript 代码。这是一个要点,也是 JavaScript 文件位置之所以十分重要的关键!

<!DOCTYPE html>
<html lang="en">
<head>
  <link rel="stylesheet" href="/css/styles.css" />
  <script>
    document.querySelector('footer').style.backgroundColor = 'purple';
  </script>

请注意,我们目前所获得的代码底部是一个 <script> 文件。这是使用内联 JavaScript,而非指向外部文件的。内联文件的执行速度会更快,因为浏览器不必再发出网络请求来获取 JavaScript 文件。但是,这个内联版本和 HTML 链接到外部 JavaScript 文件的结果将完全相同。

问题出在 .querySelector() 方法。当它运行时…所构建的文档对象模型中尚没有可供选择的 <footer> 元素!因此,它不会返回 DOM 元素,而是会返回 null。这将导致一个错误,因为它相当于运行以下代码:

null.style.backgroundColor = 'purple';

由于 null 并没有 .style 属性,因此我们的错误就出现了。我们将 JavaScript 文件移到了页面底部。想一想为什么这样做可以解决问题。答案是,如果 DOM 是连续构建的,那么将 JavaScript 代码移到页面的最底部,则当 JavaScript 代码运行的时候,所有 DOM 元素都已经存在了!

不过,一个 替代 解决方案则是使用浏览器事件!

使用 DOMContentLoaded 事件

当文档对象模型被完全加载时,浏览器将触发一个事件。这个事件被称为 DOMContentLoaded 事件,我们可以使用监听任何其他事件的方式来监听这个事件:

document.addEventListener('DOMContentLoaded', function () {
    console.log('the DOM is ready to be interacted with!');
});

如果你去查看别人的代码,你可能会发现,他们的代码监听的是正在使用的 load 事件(例如 document.onload(...))。load 会比 DOMContentLoaded 更晚触发——load 会等到所有图像、样式表等加载完毕(HTML 引用的所有东西)。很多年长的开发者会使用 load 来代替 DOMContentLoaded,因为后者不被最早的浏览器支持。但是,如果你需要检测代码的运行时间,通常 DOMContentLoaded 是更好的选择。

仅仅因为你可以使用 DOMContentLoaded 事件在 <head> 中编写 JavaScript 代码,并不意味着你就应该这样做。因为这样做的话,我们必须编写更多代码(所有事件监听器之类),而更多代码通常并不总是最好的办法。相反,更好的选择是将代码移到 HTML 文件底部,放在结束 </body> 标签之前。

什么时候应该使用这个技能呢?由于 <head> 中的 JavaScript 代码会在 <body> 中的 JavaScript 代码之前运行,因此如果你确实有 JavaScript 代码需要尽快运行,则可以将该代码放在 <head> 中,并将其包裹在一个 DOMContentLoaded 事件监听器中。这样,它既可以尽早运行,又不会在 DOM 尚未准备就绪的时候过早运行。

MDN 上的 DOMContentLoaded 事件文档

猜你喜欢

转载自blog.csdn.net/MoDuRooKie/article/details/82352866