UI 事件
一、鼠标事件
鼠标事件不单单来自于鼠标,还可能来自手机或平板,它们对此类操作进行了模拟以实现兼容。
1.1 鼠标事件类型
mousedown/mouseup
在元素上点击/释放鼠标按钮。
mouseover/mouseout
鼠标指针从一个元素上移入/移出。
mousemove
鼠标在元素上的每个移动都会触发此事件。
click
如果使用的是鼠标左键,则在同一个元素上的 mousedown 及 mouseup 相继触发后,触发该事件。
dblclick
在短时间内双击同一元素后触发。如今已经很少使用了。
contextmenu
在鼠标右键被按下时触发。还有其他打开上下文菜单的方式,例如使用特殊的键盘按键,在这种情况下它也会被触发,因此它并不完全是鼠标事件。
1.2 事件顺序
一个操作可能会触发多个事件。
在单动作触发多个事件时,事件是固定的,例如鼠标被按下时,会触发mousedown—> mouseup —>click的顺序。
1.3 鼠标按钮属性
与点击相关的事件中有一个event.button属性,这个属性可以知道用户具体点击了哪个鼠标按钮。
一般来说,我们不会在click和contextmenu事件中使用,因为我们已经知道了,前者用的是左键,后者用的是右键。
但是mousedown和mouseup事件中则可能需要用到这个属性,因为不管用户点到哪个键,都会触发这两个事件,所以我们需要知道用户到底按的是鼠标的哪个按钮。
event.button 的所有可能值如下:
| 鼠标按键状态 | event.button |
|---|---|
| 左键 (主要按键) | 0 |
| 中键 (辅助按键) | 1 |
| 右键 (次要按键) | 2 |
| X1 键 (后退按键) | 3 |
| X2 键 (前进按键) | 4 |
还有一个 event.buttons 属性,其中以整数的形式存储着当前所有按下的鼠标按键。
1.4 组合键
鼠标是可以跟键盘按键组合的,鼠标事件中会包含组合键的信息
shiftKey:ShiftaltKey:Alt(或对于 Mac 是 Opt)ctrlKey:CtrlmetaKey:对于 Mac 是 Cmd,在 Mac 上我们通常使用Cmd代替Ctrl
如果在事件期间按下了相应的键,则它们为 true。
比如,下面这个按钮仅在 Alt+Shift+click 时才有效:
html1<button id="button">Alt+Shift+Click on me!</button> 2 3<script> 4 button.onclick = function (event) { 5 if (event.altKey && event.shiftKey) { 6 alert('Hooray!') 7 } 8 } 9</script>
1.5 鼠标坐标:clientX/Y,pageX/Y
- 相对于窗口的坐标:
clientX和clientY。 - 相对于文档的坐标:
pageX和pageY。
1.6 防止在鼠标按下时的选择
双击鼠标后它会选择文本。
如果按下鼠标左键,不松开的情况下移动鼠标,也会选择文本。
如果不想要这种默认行为,最合理的方式是防止浏览器对mousedown进行操作的默认行为。
javascript1elem.addEventListener('mousedown', function (event) { 2 //这样就不会对文本选择 3 event.preventDefault() 4})
1.7 防止复制
如果我们允许用户选择文本,但是不允许用户复制内容,我们可以用另外一个事件:copy
在copy事件上禁止默认行为就可以保护页面内容不被复制。
javascript1elem.addEventListener('copy', function (event) { 2 event.preventDefault() 3})
当然,用户如果打开开发者工具还是可以复制的...
1.8 小结
鼠标有以下属性:
-
button—用户点的是鼠标的哪个键 -
组合键,如果按下则为
true:altKey,ctrlKey,shiftKey和metaKey(Mac)。mac下的metaKey等于window下的ctrlKey
-
文档坐标
pageX/Y -
窗口坐标
clientX/Y
mousedown 的默认浏览器操作是文本选择,如果不想要,则可以取消这个默认行为。
二、移动鼠标
2.1 事件 mouseover/mouseout,relatedTarget
mouseover:鼠标指针移动上去触发mouseout:鼠标指针移动出去触发

这些事件很特别,因为它们具有relatedTarget属性。这个属性是对target的补充。
当鼠标从一个元素离开去另一个元素时,其中一个元素就变成了target,另一个就变成了relatedTarget。
对于mouseover:
event.target—— 是鼠标移过的那个元素。event.relatedTarget—— 是鼠标来自的那个元素(relatedTarget→target)。
mouseout则刚好相反:
event.target—— 是鼠标移出的那个元素。event.relatedTarget—— 是鼠标当前移动到那个元素(target→relatedTarget)。
这很合理,当鼠标移入a元素时,此时触发mouseover事件,target自然是当前a元素。
当鼠标从a元素转为移入b元素时,对a来说,触发了mouseout事件,对b来说,触发了mouseover事件,所以mouseout下的target就是a,mouseover(此时为b元素触发的事件)中的relatedTarget则是a。
relatedTarget可以是null,比如鼠标来自窗口之外,或者离开了窗口。
2.2 跳过元素
当鼠标移动时,会触发mousemove事件。但不意味着每个元素都会导致一个事件。
浏览器会一直检查鼠标的位置,如果发生了变化,就会触发事件。
如果访问者非常快速地移动鼠标,那么某些 DOM 元素就可能被跳过。

#如果鼠标从上图所示的 #FROM 快速移动到 #TO 元素,则中间的 <div>(或其中的一些)元素可能会#被跳过。mouseout 事件可能会在 #FROM 上被触发,然后立即在 #TO 上触发 mouseover。
这对性能有好处,因为可能有很多中间元素,我们并不想真的要处理每一个移入和离开的过程。
鼠标指针并不会访问所有元素,它可以跳过一些元素。
因此,当鼠标从窗口外调到页面中间,这种情况下,relatedTarget就可能是null。

在鼠标快速移动下,中间元素可能会被忽略。但如果mouseover被触发了,当鼠标离开元素时,则一定会有mouseout事件
2.3 当移动到一个子元素时 mouseout
mouseout有一个重要的知识点——当鼠标指针从元素移动到其后代的时候会触发,例如下面这个结构,从##parent到#child:
html1<div id="parent"> 2 <div id="child">...</div> 3</div>
#如果我们在#parent上,将鼠标指针移到#child上,#parent会得到一个mouseout事件。

根据浏览器的逻辑,鼠标指针可能随时位于单个元素上 —— 嵌套最多的那个元素(或 z-index 最大的那个)。
因此,如果他转到另一个元素(可能是后代),那么它将离开前一个元素。
这里的事件处理有一个重要的细节:
#后代的mouseover事件会冒泡。因此如果#parent具有mouseover处理程序,它也将被触发。

#所以当鼠标移动到#child时,会触发#parent的moustout事件和mouseover事件
我们并不希望发生这种情况,我们可以通过检查relatedTarget来避免这种事的发生。
但还有一种更好的方式:mouseenter和mouseleave事件,它们没有这样的问题。
2.4 mouseenter 和 mouseleave
当鼠标指针进入一个元素时 —— 会触发 mouseenter。而鼠标指针在元素或其后代中的确切位置无关紧要。
当鼠标指针离开该元素时,事件 mouseleave 才会触发。
这两个事件跟mouseover和mouseleave差不多,但它们有以下区别:
- 在元素内部与后代之间的转换不会有影响
- 不会冒泡
当鼠标移入子元素时,就算此时触发了子元素的mouseenter,但是并不会冒泡给父元素。
2.5 事件委托
由于mouseenter和mouseleave并不会冒泡,所以我们不能用它们来进行事件委托。
但如果我们一定要用到事件委托,就只能够用mouseover和mouseout来做额外的处理。
2.6 小结
- 快速地鼠标移动可能会忽略掉某些元素
mouseover/out和mouseenter/leave事件还有一个附加属性:relatedTarget。- 如果我们从父元素转到子元素,也会触发
mouseover/out事件。浏览器假定鼠标一次只会在最深的那个元素。 mouseenter/leave跟over/out不同,它们仅在鼠标进入和离开时才会触发。它们并不会冒泡。
三、鼠标拖放事件
3.1 鼠标事件实现拖放
本质上来说,一个鼠标的位置变动就是让我们设置元素的left和top。
基本的拖放算法是这样的:
- 鼠标点击后,获取元素的坐标:元素当前相对于窗口的坐标、鼠标点击的坐标、鼠标到元素边缘的距离
- 鼠标移动,设置元素的属性为
position:absolute,并用left和top来移动它的位置 - 鼠标抬起后取消移动事件
预览:

直接贴代码:
html1<style> 2 ##ball { 3 position: absolute; 4 } 5</style> 6<body> 7 <img id="ball" src="https://js.cx/clipart/ball.svg" /> 8 <script> 9 let mouseX, mouseY, elemX, elemY, shiftX, shiftY 10 // 1 取消浏览器对图片和一些其他元素的拖放处理的默认行为 11 ball.ondragstart = function () { 12 return false 13 } 14 function onMove(event) { 15 // 4 16 ball.style.left = event.pageX - shiftX + 'px' 17 ball.style.top = event.pageY - shiftY + 'px' 18 } 19 // 2 20 ball.addEventListener('mousedown', function (event) { 21 //取坐标 22 mouseX = event.clientX //鼠标位置x 23 mouseY = event.clientY //鼠标位置y 24 elemX = this.getBoundingClientRect().left //元素的窗口位置x 25 elemY = this.getBoundingClientRect().top //元素的窗口位置Y 26 // 3 27 shiftX = mouseX - elemX //鼠标位置距离元素左边的坐标 28 shiftY = mouseY - elemY //鼠标位置距离元素右边的坐标 29 //2 向document添加移动事件监听 30 document.addEventListener('mousemove', onMove) 31 }) 32 33 ball.addEventListener('mouseup', function (event) { 34 //取消移动事件 5 35 document.removeEventListener('mousemove', onMove) 36 }) 37 </script> 38</body>
这里一共需要三个 API,分别为鼠标按下去mousedown、鼠标移动mousemove、鼠标抬起mouseup。
-
由于拖动的元素是图片,浏览器有自己的对图片和一些其他元素的拖放处理,所以要禁止默认行为
ondragstart。 -
mousemove会经常被触发,但不会针对每个像素都如此。因此,在快速移动鼠标后,鼠标指针可能会从球上跳转至文档中间的某个位置(甚至跳转至窗口外)。总而言之,快速的鼠标移动让浏览器跳过了ball上的鼠标移动事件,所以这里需要在document上跟踪mousemove,而不是ball上。 -
shiftX跟shiftY代表的是鼠标的位置距离元素左侧/上侧边缘坐标的距离。
本质上来说,一个鼠标的位置变动就是让我们设置元素的
left和top。我们需要计算鼠标在窗口的位置 - 元素在窗口的位置,就能够得到鼠标距离元素边缘的
shiftX和shiftY-
shiftX=鼠标的pageX- 元素的窗口坐标getBoundingClientRect().left -
shiftY=鼠标的pageY- 元素的窗口坐标getBoundingClientRect().top
有个太先进而导致浏览器还未全部兼容的 API,offsetX 跟 offsetY,可以直接得到 shiftX 和 shiftY 的值。
-
-
移动后的鼠标相对于文档(不是窗口)的位置减去
shiftX/Y就可以得到left和top。 -
当鼠标抬起代表移动要结束,直接删除移动事件就可以了。
3.2 放置目标
球可以放置在任何地方,下面完成一个当球放置在目标元素时,目标元素高亮的功能。
我们需要知道目标元素是什么。当我们拖动时,我们可以利用document.elementFromPoint(clientX,clientY)通过坐标的位置来知道当前被移动到的是哪个元素。
问题是我们的鼠标是一直在球这个元素上的,所以我们需要另外处理一下,否则document.elementFromPoint(clientX,clientY)将会一直返回球(因为它在最上面)。
处理方式如下:
javascript1ball.hidden = true // 先隐藏球 2currentDrop = document.elementFromPoint(event.clientX, event.clientY) //获取球下面的元素 3ball.hidden = false
当返回的元素是目标元素或者是它的子元素时,我们就可以处理让目标元素高亮的元素了。
javascript1 #if (currentDrop.closest("#div")) {//匹配球下面的元素的祖先(或自己)的id是否为目标元素的。 2 enterDroppable(div); 3 } else { 4 leaveDroppable(div); 5 }
实现效果:

完整代码:
html1<style> 2 body { 3 height: 2000px; 4 width: 100%; 5 } 6 ##ball { 7 position: absolute; 8 } 9 ##div { 10 width: 200px; 11 height: 100px; 12 border: 1px solid red; 13 } 14</style> 15<body> 16 <div id="div"></div> 17 <img id="ball" src="https://js.cx/clipart/ball.svg" /> 18 <script> 19 let mouseX, mouseY, elemX, elemY, shiftX, shiftY, currentDrop; 20 // 1 取消浏览器对图片和一些其他元素的拖放处理的默认行为 21 ball.ondragstart = function () { 22 return false; 23 }; 24 function enterDroppable(elem) { 25 elem.style.background = "red"; 26 } 27 function leaveDroppable(elem) { 28 elem.style.background = ""; 29 } 30 function onMove(event) { 31 // 4 32 ball.style.left = event.pageX - shiftX + "px"; 33 ball.style.top = event.pageY - shiftY + "px"; 34 35 ball.hidden = true; 36 currentDrop = document.elementFromPoint(event.clientX, event.clientY); 37 ball.hidden = false; 38 39 #if (currentDrop.closest("#div")) { 40 enterDroppable(div); 41 } else { 42 leaveDroppable(div); 43 } 44 } 45 // 2 46 ball.addEventListener("mousedown", function (event) { 47 //取坐标 48 mouseX = event.clientX; //鼠标位置x 49 mouseY = event.clientY; //鼠标位置y 50 elemX = this.getBoundingClientRect().left; //元素的窗口位置x 51 elemY = this.getBoundingClientRect().top; //元素的窗口位置Y 52 // 3 53 shiftX = mouseX - elemX; //鼠标位置距离元素左边的坐标 54 shiftY = mouseY - elemY; //鼠标位置距离元素右边的坐标 55 //2 向document添加移动事件监听 56 document.addEventListener("mousemove", onMove); 57 }); 58 59 ball.addEventListener("mouseup", function (event) { 60 //取消移动事件 5 61 document.removeEventListener("mousemove", onMove); 62 }); 63 </script> 64</body>
3.3 小结
基础的使用鼠标的拖放方法是这样的:
- 事件构成:
mousedown—>mousemove—>mouseup(由于浏览器对图片等有默认拖拽行为,所以这里取消dragstart的默认行为) - 拖放本质上就是给被拖放的元素设置
left和top,这里需要获取到鼠标指针对于元素的边缘的坐标距离shiftX/shiftY,也可以直接用offsetX/Y但兼容性不好 - 使用
document.elementFromPoint检测鼠标指针下的 “droppable” 的元素。 - 由于
mousemove事件触发频繁,有可能会忽略元素,所以需要设置在顶层document上。
四、键盘事件
4.1 keydown 和 keyup
keydown键盘按下keyup键盘抬起
事件对象的key属性允许获得字符,而code属性则允许获取物理按键代码。
以下以按键 z/Z 为例
| Key | event.key | event.code |
|---|---|---|
| Z | z(小写) | KeyZ |
| Shift+Z | Z(大写) | KeyZ |
| F1 | F1 | F1 |
| Backspace | Backspace | Backspace |
| Shift | Shift | ShiftRight 或 ShiftLeft |
同一个按键 Z,可以与或不与 Shift 一起按下。我们会得到两个不同的字符:小写的 z 和大写的 Z。
event.key 正是这个字符,并且它将是不同的。但是,event.code 是相同的。
event.code可以标明是左边的还是右边的 shift 被按下了。
我们要处理一个热键:Ctrl+Z(或 Mac 上的 Cmd+Z),则可以用以下代码:
javascript1document.addEventListener('keydown', function (event) { 2 if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) { 3 alert('Undo!') 4 } 5})
event.code 有一个问题。对于不同的键盘布局,相同的按键可能会具有不同的字符。
下面是美式布局(“QWERTY”)和德式布局(“QWERTZ”)

对于同一个按键,美式布局为 “Z”,而德式布局为 “Y”(字母被替换了)。
从字面上看,对于使用德式布局键盘的人来说,但他们按下 Y 时,event.code 将等于 KeyZ。
因此,event.code 可能由于意外的键盘布局而与错误的字符进行了匹配。不同键盘布局中的相同字母可能会映射到不同的物理键,从而导致了它们有不同的代码。
为了可靠地跟踪与受键盘布局影响的字符,使用 event.key 可能是一个更好的方式。
另一方面,event.code 的好处是,即使访问者更改了语言,绑定到物理键位置的 event.code 会始终保持不变。因此,即使在切换了语言的情况下,依赖于它的热键也能正常工作。
我们想要处理与布局有关的按键?那么 event.key 是我们必选的方式。
或者我们希望一个热键即使在切换了语言后,仍能正常使用?那么 event.code 可能会更好。
4.2 自动重复
如果按下一个键足够长的时间,它就会开始“自动重复”:keydown 会被一次又一次地触发,然后当按键被释放时,我们最终会得到 keyup。因此,有很多 keydown 却只有一个 keyup 是很正常的。
对于由自动重复触发的事件,event 对象的 event.repeat 属性被设置为 true。
4.3 默认行为
默认行为各不相同,因为键盘可能会触发很多可能的东西。
例如:
- 出现在屏幕上的一个字符(最明显的结果)。
- 一个字符被删除(Delete 键)。
- 滚动页面(PageDown 键)。
- 浏览器打开“保存页面”对话框(Ctrl+S)
- ……等。
阻止对 keydown 的默认行为可以取消大多数的行为,但基于 OS 的特殊按键除外。例如,在 Windows 中,Alt+F4 会关闭当前浏览器窗口。并且无法通过在 JavaScript 中阻止默认行为来阻止它。
4.4 keypress 事件
过去曾经有一个 keypress 事件,还有事件对象的 keyCode、charCode 和 which 属性。
大多数浏览器对它们都存在兼容性问题,虽然浏览器还在支持它们,但现在完全没必要再使用它们。
4.5 小结
按一个按键总是会产生一个键盘事件,无论是符号键,还是例如 Shift 或 Ctrl 等特殊按键。唯一的例外是有时会出现在笔记本电脑的键盘上的 Fn 键。它没有键盘事件,因为它通常是被在比 OS 更低的级别上实现的。
键盘事件:
keydown—— 在按下键时(如果长按按键,则将自动重复),keyup—— 释放按键时。
键盘事件的主要属性:
code—— “按键代码”("KeyA","ArrowLeft"等),特定于键盘上按键的物理位置。key—— 字符("A","a"等),对于非字符(non-character)的按键,通常具有与code相同的值。
过去,键盘事件有时会被用于跟踪表单字段中的用户输入。这并不可靠,因为输入可能来自各种来源。
五、滚动事件
scroll事件在window和可滚动元素上都可以运行。
下面是一个滚动事件的示例:
javascript1window.addEventListener('scroll', function (event) { 2 console.log(this.pageXOffset) 3 console.log(this.pageYOffset) 4})
5.1 防止滚动
我们不能通过在 onscroll 监听器中使用 event.preventDefault() 来阻止滚动,因为默认事件是在滚动发生 之后 才触发的。
我们可以设置overflow: hidden;来让防止滚动。