原文链接:https://javascript.info/introduction-browser-events,translate with ❤️ by zhangbao.
事件表示发生了某件事的信号。所有 DOM 节点都能产生这样的信号(但不限于 DOM)。
下面列举了最常用的一些事件:
鼠标事件:
click
:在元素上鼠标左击时(或在触屏设备上触碰时)。contextmenu
:在元素上鼠标右击时。mouseover
/mouseout
:当鼠标光标进入/离开元素时。mousedown
/mouseup
:在元素上按下/释放鼠标按键时。mousemove
:当鼠标移动时。
表单元素事件:
submit
:当用户提交表单(<form>
)时。focus
:当用户 focus 元素时。比如:在<input>
上。
键盘事件:
keydown
和keyup
:当用户按下/释放键盘按键时。
文档事件:
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 特性是不区分大小写的,所以 ONCLICK
、onClick
和 onCLICK
是一样的,但是通常使用全部小写的形式:onclick
。
DOM 属性
我们还可使用 elem.on<事件名>
的形式为元素绑定事件处理器。
例如,elem.onclick
:
<input id="elem" type="button" value="点击我">
<script>
elem.onclick = function () {
alert('谢谢');
};
</script>
其实使用 HTML 特性绑定的事件处理器,是这样运行的:浏览器读取属性后,创建一个新的函数,然后将函数体代码的内容设置为特性值。因此,本质上跟使用 DOM 属性的方式是一样的。
就是说,事件处理器总是绑定在 DOM 属性上,HTML 特性只是初始化它的一种方式。
下面两个代码片段在做一样的事:
- HTML 版
<input type="button" onclick="alert('点击!')" value="按钮">
- 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) });
DOM 属性区分大小写。
要使用 elem.onclick
绑定事件处理器 ,elem.ONCLICK
这种形式的写法就不行,因为 DOM 属性是区分大小写的。
addEventListener
前面提到的绑定事件的方式有个问题:同一个事件类型,不能同时绑定多个事件处理程序。
例如,在点击一个 button
的时候——button
高亮,并且展示一条提示信息。
如果我们使用之前事件绑定方式去做,那么新绑定的 DOM 属性会覆盖已存在的:
input.onclick = function () { alert(1); }
input.onclick = function () { alert(2); } // 这个事件绑定会覆盖前一个
Web 标准开发人员很久以前就意识到了这个问题,因此引入了一个替代方案:addEventListener
和 removeEventListener
,它们解决了不能为同一个事件类型绑定多个事件处理器的问题。
语法如下(添加事件处理程序):
element.addEventListener(event, handler[, phase])
event
事件名。例如,"click"
。
handler``
事件处理程序。
phase``
可选参数,指定事件处理函数在哪个“阶段”触发。之后会讲解,通常我们不会使用这个参数。
移除事件处理器,使用 removeEventListener
:
// 与 addEventListener 接收的参数一致
element.removeEventListener(event, handler[, phase]);
保证**删除的是同一个处理程序**
如果删除的与绑定的不是同一个处理程序。即使是一样的代码,这种删除也是无效的。
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
绑定事件处理器。但通常,我们只使用其中一种方式。
某些事件只能用 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
发生鼠标事件时,光标处的窗口坐标。
还有更多其他的属性,不同的事件类型也有自己的特定属性集合。这会在之后详细讨论时介绍。
事件对象也可以从 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
对象只处理了mousedown
和mouseup
事件,没有处理其他事件。
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>
现在不同事件的的处理逻辑,被单独分开,变得更加清晰了,这样的代码也更容易维护。
总结
有三种绑定事件处理器的方式:
HTML 特性:
onclick="..."
。DOM 属性:
elem.onclick = function
。方法:添加事件用
elem.addEventListener(event, handler[, phase])
,移除事件用removeEventListener
。通过 HTML 特性绑定事件的方式很少使用,因为在 HTML 标记里写 JavaScript 代码看起来有点奇怪和陌生,而且也不能写很多代码(维护苦难)。
可以使用 DOM 属性,但有一个缺点就是不能为同一个事件类型分配多个处理程序。在多数情况下,这种也许并不急迫。
最后一种方式最灵活,但写起来最长。还有某些事件只支持用 addEventListener 的方式绑定,例如
transtionend
和DOMContentLoaded
(以后会讲)。而且,addEventListener
也支持通过对象来指定要绑定的事件处理器,此种情况会调用对象的handleEvent
属性方法。不管以何种方式分配处理程序——处理程序接收的第一个参数都是事件对象,该事件对象中包含了与所发生的事件相关的所有细节信息。
我们会在接下来的章节中了解更多关于不同类型的事件信息。
练习题
问题
一、点击时隐藏
为 button 添加一个点击事件,点击时,隐藏之后的
元素。二、隐藏自己
创建一个按钮,点击时隐藏自己。
三、哪个处理程序会执行?
变量 button 中存储的是一个按钮元素,还没有为其分配事件处理器。
在执行下面的代码之后,会显示哪些 alert 内容呢?
button.addEventListener("click", () => alert("1")); button.removeEventListener("click", () => alert("1")); button.onclick = () => alert(2);
四、在球场中移动球
在绿色球场中点击时,球移动至点击处。
要求:
移动后,球的中心点位于鼠标点击处(在没有越过球场边缘的情况下)。
欢迎使用 CSS 动画。
球不能超出球场边界。
滚动页面时,一切都正常。
注意:
代码在不同大小的球场和不同尺寸球的情况下,都能正常工作。不要使用任何的固定值赋值。
使用 event.clienX/event.clientY 来获取点击处的坐标值。
五、创建一个滑动菜单
创建一个菜单,提供点击时打开/收起子菜单项的功能。
收起状态: 打开状态:PS. 为了达到效果,在打开/收起子菜单项时,会修改文档内容。
六、添加关闭按钮
这里有一列消息。
使用 JavaScript,在每条消息的右上角添加一个关闭按钮。
结果看起来是这样的:
HTML 结构如下:
<div class="pane"> <h3>Horse</h3> <p>The horse is one of...</p> </div> <div class="pane">...</div>
七、轮播图
创建一个轮播图——一种可以通过点击箭头来滚动图片的组件。
之后我们会对此轮播图添加更多功能:无限滚动,动态加载等。
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 属性设置正确的值了,它们是相对于球场的坐标值。
这是图示:
我们使用 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>
我们为其设置了 onclick,在整个水平方向上,点击事件都会被捕获。
但 是行内元素,元素的宽度是由内容撑开的:
<span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>
切换菜单
切换菜单的时候,需要:改变箭头,并且展示/隐藏菜单列表。
这些样式可以用 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>');
然后