[TOC]

原文链接:https://javascript.info/introduction-browser-events,translate with ❤️ by zhangbao.

事件表示发生了某件事的信号。所有 DOM 节点都能产生这样的信号(但不限于 DOM)。

下面列举了最常用的一些事件:

鼠标事件:

  • click:在元素上鼠标左击时(或在触屏设备上触碰时)。

  • contextmenu:在元素上鼠标右击时。

  • mouseover/mouseout:当鼠标光标进入/离开元素时。

  • mousedown/mouseup:在元素上按下/释放鼠标按键时。

  • mousemove:当鼠标移动时。

表单元素事件:

  • submit:当用户提交表单(<form>)时。

  • focus:当用户 focus 元素时。比如:在 <input> 上。

键盘事件:

  • keydownkeyup:当用户按下/释放键盘按键时。

文档事件:

  • DOMContentLoaded:当 HTML 的 DOM 树构建完毕后。

CSS 事件:

  • transitionend:当 CSS 过度动画结束时。

还有许多其他事件,我们会在后面章节里详细针对地阐述。

事件处理器

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

处理程序是在用户操作过程中,运行 JavaScript 代码的一种方式。

有几种分配处理程序的方式。我们先从最简单的开始讲。

HTML 特性

事件处理器可以作为特性在 HTML 标签中使用,特性以 on<事件名> 形式命名。

例如,为 input 绑定一个 click 事件处理器,就使用 onclick,像这样:

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

当在 input 中点击鼠标时,就会执行 onclick 中的代码。

请注意, 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 属性

我们还可使用 elem.on<事件名> 的形式为元素绑定事件处理器。

例如,elem.onclick

<input id="elem" type="button" value="点击我">
<script>
  elem.onclick = function () {
    alert('谢谢');
  };
</script>

其实使用 HTML 特性绑定的事件处理器,是这样运行的:浏览器读取属性后,创建一个新的函数,然后将函数体代码的内容设置为特性值。因此,本质上跟使用 DOM 属性的方式是一样的。

就是说,事件处理器总是绑定在 DOM 属性上,HTML 特性只是初始化它的一种方式。

下面两个代码片段在做一样的事:

  1. HTML 版
<input type="button" onclick="alert('点击!')" value="按钮">
  1. HTML + JS 版
<input type="button" id="button" value="按钮">
<script>
  button.onclick = function() {
    alert('点击!');
  };
</script>

因为我们只有一个 onclick 属性,导致我们只能给点击事件绑定一个事件处理器。

比如说下面的例子,会导致后面绑定的事件处理器覆盖已存在的:

<input type="button" id="elem" onclick="alert('Before')" value="点击我">
<script>
  elem.onclick = function() { // 覆盖了 HTML 特性 onclick 中的代码
    alert('After'); // 点击事件发生时,只会弹出“After”
  };
</script>

顺便说一下,我们可以直接将现有函数作为事件处理程序使用。

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

elem.onlick = sayThanks;

删除一个事件处理器,使用 elem.onclick = null

使用 this 访问元素

事件处理函数中的 this 指代绑定事件的元素。

下面例子中,我们点击 button 后,会弹出按钮的内容( this.innerHTML):

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

可能会犯的错误

最开始写事件处理代码的时候,要注意一些容易犯错的地方。

以 DOM 属性方式绑定的事件处理器,应该用 sayThanks,而不是 sayThanks()

// 正确 ✔
button.click = sayThanks;

// 错误 ✖
button.onclick = sayThanks();

如果在函数名之后加圆括号(sayThanks()),绑定的是函数执行结果,所有相当于给 onclick 属性绑定了 undefined(因为函数没有返回值),肯定是无效的。

但在 HTML 特性里绑定的事件处理器就需要加圆括号了:

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

这种区别很容易解释。当浏览器读取 HTML 特性的时候,会创建一个新的处理函数,并将特性值作为新函数的函数体。

所以,上面例子中的代码等同于:

button.onclick = function () {
  sayThanks(); // 特性内容
};

使用函数,而不是字符串。

如果我们使用 elem.onclick = "alert(1)" 的形式也能成功绑定事件,但这种写法只是为了向前兼容,并不建议使用这种方式。

不要使用setAttribute 设置事件处理函数。

下面种方式的调用不会起作用:

// 因为特性值类型都是字符串,所有这里的函数也被转换成字符串了
document.body.setAttribute('onclick', function() { alert(1) });

浏览器事件 - 图1

DOM 属性区分大小写。

要使用 elem.onclick 绑定事件处理器 ,elem.ONCLICK 这种形式的写法就不行,因为 DOM 属性是区分大小写的。

addEventListener

前面提到的绑定事件的方式有个问题:同一个事件类型,不能同时绑定多个事件处理程序。

例如,在点击一个 button 的时候——button 高亮,并且展示一条提示信息。

如果我们使用之前事件绑定方式去做,那么新绑定的 DOM 属性会覆盖已存在的:

input.onclick = function () { alert(1); }
input.onclick = function () { alert(2); } // 这个事件绑定会覆盖前一个

Web 标准开发人员很久以前就意识到了这个问题,因此引入了一个替代方案:addEventListenerremoveEventListener,它们解决了不能为同一个事件类型绑定多个事件处理器的问题。

语法如下(添加事件处理程序):

element.addEventListener(event, handler[, phase])

event

事件名。例如,"click"

handler``

事件处理程序。

phase``

可选参数,指定事件处理函数在哪个“阶段”触发。之后会讲解,通常我们不会使用这个参数。

移除事件处理器,使用 removeEventListener

// 与 addEventListener 接收的参数一致
element.removeEventListener(event, handler[, phase]);

浏览器事件 - 图2保证**删除的是同一个处理程序**

如果删除的与绑定的不是同一个处理程序。即使是一样的代码,这种删除也是无效的。

elem.addEventListener( 'click' , () => alert('谢谢!'));
elem.removeEventListener( 'click', () => alert('谢谢!'));

之前绑定的事件处理器并没删掉,因为 removeEventLisener 删除的是另一个函数:虽然代码内容一致,但并非是同一个函数。

```javascript function handler() { alert( ‘谢谢!’ ); }

input.addEventListener(‘click’, handler); input.removeEventListener(‘click’, handler);

> 
> 记住,如果我们不把绑定的事件处理函数保存在一个变量中,那么之后也就无法删除。因为在 `addEventListener `之后没有任何方式“回溯”,找到之前绑定的那个函数了。


使用 `addEventListener`  可以实现同一个事件类型绑定多个事件处理器,像这样:

```html
<input id="elem" type="button" value="点击我"/>

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

  function handler2() {
    alert('再次感谢!');
  }

  // 点击 elem 后
  elem.onclick = () => alert('你好');      // 打印 你好
  elem.addEventListener('click', handler1); // 打印 Thanks!
  elem.addEventListener('click', handler2); // 打印 Thanks again!
</script>

我们可以同时使用 DOM 属性和 addEventListener 绑定事件处理器。但通常,我们只使用其中一种方式。

浏览器事件 - 图3某些事件只能用 addEventListener`` 的方式进行绑定。

某些事件不能用 DOM 属性方式绑定,而只能用 addEventListener`` 的方式:

例如,transitionend`` 事件(过渡动画完成后)。

下面的代码,在大多数浏览器中,只有第二个事件处理器会生效,第一个就不行:

```html



<a name="5eoqey"></a>
## 事件对象

为了精确处理事件,我们想知道更多的“发生了什么”的事件信息,而不是仅仅是知道“点击了”或者“按下了”。试想,如何获得鼠标点击时坐标位置?或者当前按下的是哪个按键?

事件发生时,浏览器会创建一个事件对象,包含了与发生事件相关的细节信息。事件对象本身会作为参数传递给事件处理器。

下面例子中,从事件对象中获取鼠标点击时,点击处的窗口坐标:

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

<script>
  elem.onclick = function(event) {
    // 显示事件类型,绑定事件的元素和鼠标点击处的窗口坐标
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>

事件对象(event )的一些属性:

event.type

事件类型,例如 "click"

event.currentTarget

表示绑定事件的元素,即 this(排序手动指定事件上下文环境的情况)。event.currentTarget 还是挺有用的。

event.clientX/event.clientY

发生鼠标事件时,光标处的窗口坐标。

还有更多其他的属性,不同的事件类型也有自己的特定属性集合。这会在之后详细讨论时介绍。

浏览器事件 - 图4事件对象也可以从 HTML 中获取

如果我们在 HTML 中绑定的事件处理器,我们也可以用事件对象 event`` ,像这样:

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

之所以可以这样做,是因为浏览器会读取特性时,会创建一个像这样的处理器:function(event) { alert(ecent.type) },就是说,使用的参数就叫 "event",函数体内容赋值为特性值。

对象处理器:handleEvent

我们也可以在 addEventListener 中使用对象来指定要绑定事件处理器。事件发生时,对象的 handleEvent 方法属性会遭调用。

例如:

<button id="elem">点击我</button>
<script>
elem.addEventListener('click', {
  handleEvent (event) {
    alert(event.type + ' at ' + event.currentTarget);
  }
})
</script>

也就是说,当 addEventListener 接受对象作为处理器的时候,事件发生后,会调用对象上的 handleEvent() 方法属性。

我们也可以用类,来作这些事情:

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

<script>
  class Menu {
    handleEvent (event) {
      switch (event.type) {
        case 'mousedown':
          elem.innerHTML = '按下了鼠标按键';
          break;
        case 'mouseup':
          elem.innerHTML = '...释放了按键。'
      }
    }
  }

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

这个 menu 对象能处理两种事件类型。

请注意,我们需要使用 addEventListener 显式指定要绑定的事件类型。这里,menu 对象只处理了 mousedownmouseup 事件,没有处理其他事件。

handleEvent 方法中没有必要把所有的事件处理逻辑都写上,可以针对每种事件类型,去写一个处理方法,将不同事件的处理逻辑分开:

<button id="elem">点击我</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 = '按下了鼠标按键'
    }

    onMouseup () {
      elem.innerHTML = '...释放了按键。'
    }
  }

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

现在不同事件的的处理逻辑,被单独分开,变得更加清晰了,这样的代码也更容易维护。

总结

有三种绑定事件处理器的方式:

  1. HTML 特性:onclick="..."

  2. DOM 属性:elem.onclick = function

  3. 方法:添加事件用 elem.addEventListener(event, handler[, phase]),移除事件用 removeEventListener

通过 HTML 特性绑定事件的方式很少使用,因为在 HTML 标记里写 JavaScript 代码看起来有点奇怪和陌生,而且也不能写很多代码(维护苦难)。

可以使用 DOM 属性,但有一个缺点就是不能为同一个事件类型分配多个处理程序。在多数情况下,这种也许并不急迫。

最后一种方式最灵活,但写起来最长。还有某些事件只支持用 addEventListener 的方式绑定,例如 transtionendDOMContentLoaded(以后会讲)。而且,addEventListener 也支持通过对象来指定要绑定的事件处理器,此种情况会调用对象的 handleEvent 属性方法。

不管以何种方式分配处理程序——处理程序接收的第一个参数都是事件对象,该事件对象中包含了与所发生的事件相关的所有细节信息。

我们会在接下来的章节中了解更多关于不同类型的事件信息。

练习题

问题

一、点击时隐藏

为 button 添加一个点击事件,点击时,隐藏之后的

元素。

二、隐藏自己

创建一个按钮,点击时隐藏自己。

三、哪个处理程序会执行?

变量 button 中存储的是一个按钮元素,还没有为其分配事件处理器。

在执行下面的代码之后,会显示哪些 alert 内容呢?

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

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

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

四、在球场中移动球

在绿色球场中点击时,球移动至点击处。

浏览器事件 - 图5

要求:

  • 移动后,球的中心点位于鼠标点击处(在没有越过球场边缘的情况下)。

  • 欢迎使用 CSS 动画。

  • 球不能超出球场边界。

  • 滚动页面时,一切都正常。

注意:

  • 代码在不同大小的球场和不同尺寸球的情况下,都能正常工作。不要使用任何的固定值赋值。

  • 使用 event.clienX/event.clientY 来获取点击处的坐标值。

五、创建一个滑动菜单

创建一个菜单,提供点击时打开/收起子菜单项的功能。
收起状态:浏览器事件 - 图6 打开状态:浏览器事件 - 图7

PS. 为了达到效果,在打开/收起子菜单项时,会修改文档内容。

六、添加关闭按钮

这里有一列消息。

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

结果看起来是这样的:

浏览器事件 - 图8

HTML 结构如下:

<div class="pane">
  <h3>Horse</h3>
  <p>The horse is one of...</p>
</div>
<div class="pane">...</div>

七、轮播图

创建一个轮播图——一种可以通过点击箭头来滚动图片的组件。

浏览器事件 - 图9

之后我们会对此轮播图添加更多功能:无限滚动,动态加载等。

PS. 对于这个任务,写好 HTML/CSS 结构可以说完成了整个任务量的 90%。

答案

一、点击时隐藏

<input type="button" id="hider" value="点击隐藏文本" />
<div id="text">文本</div>

<script>
// 隐藏文本的方式不止一种
// 还可以使用 style.display 隐藏
document.getElementById('hider').onclick = function() {
  document.getElementById('text').hidden = true;
}
</script>

二、隐藏自己

在处理器中使用 this 来引用事件的元素自己。

<input type="button" onclick="this.hidden=true" value="点击隐藏">

三、哪个处理程序会执行?

答案是第一个和第三个。

会触发第一个处理器,是因为它没有被 removeEventListener 移除掉。移除处理器是,我们需要保证是同一个的处理器。而代码中移除的是一个新函数,虽然代码一样,却是另一个函数。

为了移除一个函数对象,我们需要用到引用,像这样:

function handler() {
  alert(1);
}

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

button.onclick 与 addEventListener 的工作机制不一样,在使用了后者的情况下,前者仍能独立工作。

四、在球场中移动球

首先我们要考虑使用何种方式,对球定位。

我们不能使用 position: fiexed,因此这是相对窗口定位的,一旦滚动页面,那么球和球场的相对位置就偏离了。

因此,我们应该使用 position: absolute,为了相对球场定位,还需要将 field 设置成定位元素。

然后这个球就相对球场定位了:

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

#ball {
  position: absolute;
  left: 0; /* 相对于最近的定位祖先元素进行定位 (field) */
  top: 0;
  transition: 1s all; /* CSS 动画的添加让 left/top 的变化更加顺滑,也就是球的移动更加顺滑了 */
}

接下来,我们就要为 ball.style.position.left/top 属性设置正确的值了,它们是相对于球场的坐标值。

这是图示:

浏览器事件 - 图10

我们使用 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 时,必须知道球的 width/height,所以应该在 HTML 或 CSS 中指定。

<div id="field">
    <img src="https://en.js.cx/clipart/ball.svg" id="ball">
</div>

<script>
field.onclick = function(event) {

  // 相对于窗口的球场坐标
  let fieldCoords = this.getBoundingClientRect();

  // 球使用了 position:absolute 定位,球场使用了 position:relative 定位
  // 因此球是相对于球场内部的左上角定位的。
  let ballCoords = {
    top: event.clientY - fieldCoords.top - field.clientTop - ball.clientHeight / 2,
    left: event.clientX - fieldCoords.left - field.clientLeft - ball.clientWidth / 2
  };

  // 避免 top 值越出球场边界
  if (ballCoords.top < 0) ballCoords.top = 0;

  // 避免 left 值越出球场边界
  if (ballCoords.left < 0) ballCoords.left = 0;

  // 避免球的右边越出球场边界
  if (ballCoords.left + ball.clientWidth > field.clientWidth) {
    ballCoords.left = field.clientWidth - ball.clientWidth;
  }

  // 避免球的底边越出球场边界
  if (ballCoords.top + ball.clientHeight > field.clientHeight) {
    ballCoords.top = field.clientHeight - ball.clientHeight;
  }

  ball.style.left = ballCoords.left + 'px';
  ball.style.top = ballCoords.top + 'px';
}
</script>

在线例子

五、创建一个滑动菜单

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>

我们使用 来显示标题,因为

是块状元素,因此会 100% 占据水平宽度。

像这样:

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

浏览器事件 - 图11

我们为其设置了 onclick,在整个水平方向上,点击事件都会被捕获。

是行内元素,元素的宽度是由内容撑开的:

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

浏览器事件 - 图12

切换菜单

切换菜单的时候,需要:改变箭头,并且展示/隐藏菜单列表。

这些样式可以用 CSS 类来完美控制。在 JavaScript 中,我们通过添加/删除类来标记菜单的当前状态。

默认菜单是关闭的:

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

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

添加类名 .open 之后,箭头改变、显示菜单项列表:

.menu.open ul {
  display: block;
}

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

而 JavaScript 代码就比较简单了。

let menuElem = document.getElementById('sweeties');
let titleElem = menuElem.querySelector('.title');

titleElem.onclick = function() {
  menuElem.classList.toggle('open');
};

在线例子

六、添加关闭按钮

添加的按钮可以使用 position: absolute(这样的话,.pane 要加 position: relative)或者 float: right。float: right 带来的好处是按钮不会遮盖文本,但是使用 position: absolute 的话,能获得最大的自由度。就看你怎么选择了。

针对每个 .pane 我们可以执行下列的代码:

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

然后