2022 年 9 月 21 日

浏览器事件简介

事件是表示发生了某事的信号。所有 DOM 节点都会生成此类信号(但事件并不局限于 DOM)。

下面列出了一些最常用的 DOM 事件,仅供参考

鼠标事件

  • click – 当鼠标点击元素时(触摸屏设备会在轻触时生成该事件)。
  • contextmenu – 当鼠标右键点击元素时。
  • mouseover / mouseout – 当鼠标光标移入 / 移出元素时。
  • mousedown / mouseup – 当鼠标按钮在元素上按下 / 释放时。
  • mousemove – 当鼠标移动时。

键盘事件

  • keydownkeyup – 当键盘键被按下和释放时。

表单元素事件

  • submit – 当访问者提交 <form> 时。
  • focus – 当访问者聚焦于一个元素时,例如 <input>

文档事件

  • DOMContentLoaded – 当 HTML 被加载和处理,DOM 完全构建时。

CSS 事件

  • transitionend – 当 CSS 动画结束时。

还有许多其他事件。我们将在即将到来的章节中详细介绍特定事件。

事件处理程序

为了对事件做出反应,我们可以分配一个处理程序 – 一个在事件发生时运行的函数。

处理程序是一种在用户操作时运行 JavaScript 代码的方法。

有几种方法可以分配一个处理程序。让我们从最简单的一个开始。

HTML 属性

可以使用名为 on<event> 的属性在 HTML 中设置处理程序。

例如,要为 input 分配 click 处理程序,我们可以使用 onclick,如下所示

<input value="Click me" onclick="alert('Click!')" type="button">

鼠标单击时,onclick 中的代码运行。

请注意,在 onclick 中我们使用单引号,因为属性本身是用双引号引起来的。如果我们忘记代码在属性中,并在其中使用双引号,如下所示:onclick="alert("Click!")",那么它将无法正常工作。

HTML 属性不是编写大量代码的方便之处,所以我们最好创建一个 JavaScript 函数并在那里调用它。

这里单击运行函数 countRabbits()

<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Rabbit number " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Count rabbits!">

我们知道,HTML 属性名称不区分大小写,所以 ONCLICKonClickonCLICK…一样,但通常属性是小写的:onclick

DOM 属性

我们可以使用 DOM 属性 on<event> 分配一个处理程序。

例如,elem.onclick

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>

如果使用 HTML 属性分配处理程序,则浏览器会读取它,从属性内容创建一个新函数并将其写入 DOM 属性。

所以这种方式实际上与前一种方式相同。

这两个代码片段的工作方式相同

  1. 仅 HTML

    <input type="button" onclick="alert('Click!')" value="Button">
  2. HTML + JS

    <input type="button" id="button" value="Button">
    <script>
      button.onclick = function() {
        alert('Click!');
      };
    </script>

在第一个示例中,HTML 属性用于初始化 button.onclick,而在第二个示例中 – 脚本,这就是全部区别。

由于只有一个 onclick 属性,因此我们不能分配多个事件处理程序。

在下面的示例中,使用 JavaScript 添加处理程序会覆盖现有的处理程序

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // overwrites the existing handler
    alert('After'); // only this will be shown
  };
</script>

要删除处理程序 – 分配 elem.onclick = null

访问元素:this

处理程序中 this 的值是元素。具有处理程序的那个元素。

在下面的代码中,button 使用 this.innerHTML 显示其内容

<button onclick="alert(this.innerHTML)">Click me</button>

可能的错误

如果您开始使用事件,请注意一些细微之处。

我们可以将现有函数设置为处理程序

function sayThanks() {
  alert('Thanks!');
}

elem.onclick = sayThanks;

但请小心:应将该函数分配为 sayThanks,而不是 sayThanks()

// right
button.onclick = sayThanks;

// wrong
button.onclick = sayThanks();

如果我们添加括号,则 sayThanks() 将变为函数调用。因此,最后一行实际上获取函数执行的结果,即 undefined(因为函数不返回任何内容),并将其分配给 onclick。这不起作用。

…另一方面,在标记中,我们确实需要括号

<input type="button" id="button" onclick="sayThanks()">

这种差异很容易解释。当浏览器读取属性时,它会创建一个处理程序函数,其主体来自属性内容。

因此,标记会生成此属性

button.onclick = function() {
  sayThanks(); // <-- the attribute content goes here
};

请勿将 setAttribute 用于处理程序。

这样的调用不起作用

// a click on <body> will generate errors,
// because attributes are always strings, function becomes a string
document.body.setAttribute('onclick', function() { alert(1) });

DOM 属性区分大小写。

将处理程序分配给 elem.onclick,而不是 elem.ONCLICK,因为 DOM 属性区分大小写。

addEventListener

上述分配处理程序方式的基本问题是,我们无法为一个事件分配多个处理程序

比方说,我们代码的一部分希望在单击时突出显示按钮,而另一部分希望在同一单击时显示消息。

我们希望为此分配两个事件处理程序。但新的 DOM 属性会覆盖现有的属性

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // replaces the previous handler

网络标准的开发人员很早以前就理解了这一点,并提出了一种使用特殊方法 addEventListenerremoveEventListener 来管理处理程序的替代方法,不受此类约束的限制。

添加处理程序的语法

element.addEventListener(event, handler, [options]);
event
事件名称,例如 "click"
handler
处理程序函数。
options
具有以下属性的附加可选对象
  • once:如果为 true,则监听器在触发后会自动移除。
  • capture:处理事件的阶段,将在本章的后面部分 冒泡和捕获 中介绍。出于历史原因,options 也可以是 false/true,这与 {capture: false/true} 相同。
  • passive:如果为 true,则处理程序不会调用 preventDefault(),我们将在 浏览器的默认操作 中对此进行解释。

要移除处理程序,请使用 removeEventListener

element.removeEventListener(event, handler, [options]);
移除需要相同的函数

要移除一个处理程序,我们应该传递与分配的函数完全相同的一个函数。

这不起作用

elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));

处理程序不会被移除,因为 removeEventListener 获取另一个函数——具有相同的代码,但这无关紧要,因为它是一个不同的函数对象。

这是正确的方法

function handler() {
  alert( 'Thanks!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

请注意——如果我们不将函数存储在变量中,那么我们就无法移除它。没有办法“回读”由 addEventListener 分配的处理程序。

多次调用 addEventListener 允许它添加多个处理程序,如下所示

<input id="elem" type="button" value="Click me"/>

<script>
  function handler1() {
    alert('Thanks!');
  };

  function handler2() {
    alert('Thanks again!');
  }

  elem.onclick = () => alert("Hello");
  elem.addEventListener("click", handler1); // Thanks!
  elem.addEventListener("click", handler2); // Thanks again!
</script>

正如我们在上面的示例中看到的,我们可以使用 DOM 属性和 addEventListener 来设置处理程序。但通常我们只使用其中一种方法。

对于某些事件,处理程序只适用于 addEventListener

存在无法通过 DOM 属性分配的事件。只能使用 addEventListener

例如,DOMContentLoaded 事件,当文档加载且 DOM 已构建时触发。

// will never run
document.onDOMContentLoaded = function() {
  alert("DOM built");
};
// this way it works
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM built");
});

因此,addEventListener 更通用。虽然,此类事件是例外,而不是规则。

事件对象

为了正确处理事件,我们希望更多地了解所发生的事情。不仅仅是“点击”或“按下键”,而是指针坐标是什么?按下了哪个键?等等。

当事件发生时,浏览器会创建一个事件对象,将详细信息放入其中,并将其作为参数传递给处理程序。

以下是从事件对象获取指针坐标的示例

<input type="button" value="Click me" id="elem">

<script>
  elem.onclick = function(event) {
    // show event type, element and coordinates of the click
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>

event 对象的一些属性

event.type
事件类型,此处为 "click"
event.currentTarget
处理事件的元素。这与 this 完全相同,除非处理程序是箭头函数,或其 this 绑定到其他内容,那么我们可以从 event.currentTarget 获取元素。
event.clientX / event.clientY
对于指针事件,光标相对于窗口的坐标。

还有更多属性。其中许多取决于事件类型:键盘事件有一组属性,指针事件——另一组,当我们继续了解不同事件的详细信息时,我们稍后将研究它们。

事件对象也适用于 HTML 处理程序

如果我们在 HTML 中分配一个处理程序,我们也可以使用 event 对象,如下所示

<input type="button" onclick="alert(event.type)" value="Event type">

这是可能的,因为当浏览器读取属性时,它会创建一个这样的处理程序:function(event) { alert(event.type) }。也就是说:它的第一个参数称为 "event",主体取自属性。

对象处理程序:handleEvent

我们不仅可以分配一个函数,还可以使用 addEventListener 将一个对象分配为事件处理程序。当事件发生时,将调用其 handleEvent 方法。

例如

<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>

正如我们所看到的,当 addEventListener 接收一个对象作为处理程序时,它会在事件发生时调用 obj.handleEvent(event)

我们还可以使用自定义类的对象,如下所示

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      switch(event.type) {
        case 'mousedown':
          elem.innerHTML = "Mouse button pressed";
          break;
        case 'mouseup':
          elem.innerHTML += "...and released.";
          break;
      }
    }
  }

  let menu = new Menu();

  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

在这里,同一个对象处理这两个事件。请注意,我们需要使用 addEventListener 明确设置要监听的事件。menu 对象在这里仅获取 mousedownmouseup,而不是任何其他类型的事件。

方法 handleEvent 不必自己完成所有工作。它可以调用其他特定于事件的方法,如下所示

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

现在事件处理程序被明确分开,这可能更容易支持。

总结

有 3 种分配事件处理程序的方法

  1. HTML 属性:onclick="..."
  2. DOM 属性:elem.onclick = function
  3. 方法:elem.addEventListener(event, handler[, phase]) 用于添加,removeEventListener 用于移除。

HTML 属性很少使用,因为 HTML 标签中间的 JavaScript 看起来有点奇怪和陌生。而且也不能在其中编写大量代码。

DOM 属性可以使用,但我们不能为特定事件分配多个处理程序。在许多情况下,这种限制并不紧迫。

最后一种方法是最灵活的,但也是最长的。有一些事件只能使用它,例如 transitionendDOMContentLoaded(待介绍)。此外,addEventListener 支持将对象作为事件处理程序。在这种情况下,将在事件发生时调用方法 handleEvent

无论如何分配处理程序,它都会将事件对象作为第一个参数获取。该对象包含有关所发生事件的详细信息。

我们将在下一章中详细了解事件以及不同类型的事件。

任务

重要性:5

button 中添加 JavaScript,使 <div id="text"> 在我们点击它时消失。

演示

为任务打开沙盒。

重要性:5

创建一个在点击时隐藏自身的按钮。

如下所示:

可以在处理程序中使用 this 来引用此处的“元素本身”

<input type="button" onclick="this.hidden=true" value="Click to hide">
重要性:5

变量中有一个按钮。它上面没有处理程序。

在以下代码之后,哪些处理程序在点击时运行?哪些警报显示?

button.addEventListener("click", () => alert("1"));

button.removeEventListener("click", () => alert("1"));

button.onclick = () => alert(2);

答案:12

第一个处理程序触发,因为它没有被 removeEventListener 删除。要删除处理程序,我们需要传递与分配的函数完全相同的函数。而在代码中传递了一个新函数,它看起来相同,但仍然是另一个函数。

要删除函数对象,我们需要存储对它的引用,如下所示

function handler() {
  alert(1);
}

button.addEventListener("click", handler);
button.removeEventListener("click", handler);

处理程序 button.onclick 独立于 addEventListener 并且在 addEventListener 之外工作。

重要性:5

将球移动到点击位置。如下所示

要求

  • 球的中心应在点击时正好位于指针下方(如果可能,不越过场地的边缘)。
  • 欢迎使用 CSS 动画。
  • 球不得越过场地边界。
  • 当页面滚动时,任何内容都不应中断。

备注

  • 代码还应适用于不同大小的球和场地,而不受任何固定值约束。
  • 使用属性 event.clientX/event.clientY 获取点击坐标。

为任务打开沙盒。

首先,我们需要选择一种定位球的方法。

我们不能为此使用 position:fixed,因为滚动页面会将球从场地移开。

因此,我们应该使用 position:absolute,并且为了使定位真正牢固,使 field 本身定位。

然后,球将相对于场地定位

#field {
  width: 200px;
  height: 150px;
  position: relative;
}

#ball {
  position: absolute;
  left: 0; /* relative to the closest positioned ancestor (field) */
  top: 0;
  transition: 1s all; /* CSS animation for left/top makes the ball fly */
}

接下来,我们需要分配正确的 ball.style.left/top。它们现在包含相对于场地的坐标。

以下是图片

我们有 event.clientX/clientY – 点击的相对于窗口的坐标。

要获取点击的相对于场地的 left 坐标,我们可以减去场地的左边缘和边框宽度

let left = event.clientX - fieldCoords.left - field.clientLeft;

通常,ball.style.left 表示“元素(球)的左边缘”。因此,如果我们分配该 left,则球边缘(而非中心)将位于鼠标光标下方。

我们需要将球向左移动一半宽度,向上移动一半高度,使其居中。

因此,最终的 left 将是

let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2;

垂直坐标使用相同的逻辑计算。

请注意,在访问 ball.offsetWidth 时,必须知道球的宽度/高度。应在 HTML 或 CSS 中指定。

在沙盒中打开解决方案。

重要性:5

创建单击时打开/折叠的菜单

P.S. 应修改源文档的 HTML/CSS。

为任务打开沙盒。

HTML/CSS

首先,让我们创建 HTML/CSS。

菜单是页面上的独立图形组件,因此最好将其放入单个 DOM 元素中。

菜单项列表可以作为列表 ul/li 布局。

以下是示例结构

<div class="menu">
  <span class="title">Sweeties (click me)!</span>
  <ul>
    <li>Cake</li>
    <li>Donut</li>
    <li>Honey</li>
  </ul>
</div>

我们对标题使用 <span>,因为 <div> 在其上具有隐式 display:block,它将占据 100% 的水平宽度。

像这样

<div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div>

因此,如果我们在其上设置 onclick,它将捕获文本右侧的点击。

由于 <span> 具有隐式 display: inline,它只占据足够的空间以容纳所有文本

<span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>

切换菜单

切换菜单应更改箭头并显示/隐藏菜单列表。

所有这些更改都由 CSS 完美处理。在 JavaScript 中,我们应通过添加/移除类 .open 来标记菜单的当前状态。

如果没有它,菜单将关闭

.menu ul {
  margin: 0;
  list-style: none;
  padding-left: 20px;
  display: none;
}

.menu .title::before {
  content: '▶ ';
  font-size: 80%;
  color: green;
}

…而使用 .open,箭头将更改,列表将显示

.menu.open .title::before {
  content: '▼ ';
}

.menu.open ul {
  display: block;
}

在沙盒中打开解决方案。

重要性:5

有一条消息列表。

使用 JavaScript 为每个消息的右上角添加一个关闭按钮。

结果应如下所示

为任务打开沙盒。

要添加按钮,我们可以使用 position:absolute(并使窗格 position:relative)或 float:rightfloat:right 的好处是按钮永远不会与文本重叠,但 position:absolute 提供了更大的自由度。因此,选择权在你。

然后,对于每个窗格,代码可以如下所示

pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');

然后 <button> 变成 pane.firstChild,因此我们可以像这样为其添加一个处理程序

pane.firstChild.onclick = () => pane.remove();

在沙盒中打开解决方案。

重要性:4

创建一个“走马灯”——一个可以通过单击箭头来滚动的图像色带。

之后我们可以添加更多功能:无限滚动、动态加载等。

P.S. 对于此任务,HTML/CSS 结构实际上是解决方案的 90%。

为任务打开沙盒。

图片色带可以用 ul/li 列表来表示,其中包含图片 <img>

通常,这样的色带很宽,但我们用一个固定大小的 <div> 将其包围起来以“裁剪”它,以便仅显示色带的一部分

要使列表水平显示,我们需要为 <li> 应用正确的 CSS 属性,比如 display: inline-block

对于 <img>,我们还应该调整 display,因为默认情况下它是 inline。在 inline 元素下保留了额外的空间用于“字母尾部”,因此我们可以使用 display:block 来移除它。

要进行滚动,我们可以移动 <ul>。有很多方法可以做到这一点,例如通过更改 margin-left 或(更好的性能)使用 transform: translateX()

外部 <div> 有一个固定宽度,因此“多余”的图片被裁剪掉了。

整个轮播是一个页面上的独立“图形组件”,因此我们最好将其包装到一个 <div class="carousel"> 中,并在其中设置样式。

在沙盒中打开解决方案。

教程地图

评论

在评论之前先阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行,请将其包装在 <pre> 标记中,对于 10 行以上,请使用沙盒 (plnkrjsbincodepen…)