事件 mouseover/mouseout,relatedTarget
当鼠标指针移到某个元素上时,mouseover 事件就会发生,而当鼠标离开该元素时,mouseout 事件就会发生。
这些事件很特别,因为它们具有 relatedTarget 属性。此属性是对 target 的补充。当鼠标从一个元素离开并去往另一个元素时,其中一个元素就变成了 target,另一个就变成了 relatedTarget。
对于 mouseover:
- event.target —— 是鼠标移过的那个元素。
 - event.relatedTarget —— 是鼠标来自的那个元素(relatedTarget → target)。
 
mouseout 则与之相反:
- event.target —— 是鼠标离开的元素。
 - event.relatedTarget —— 是鼠标移动到的,当前指针位置下的元素(target → relatedTarget)。
 
在下面这个示例中,每张脸及其功能都是单独的元素。当你移动鼠标时,你可以在文本区域中看到鼠标事件。
每个事件都具有关于 target 和 relatedTarget 的信息:
<!DOCTYPE HTML><html><head><meta charset="utf-8"><link rel="stylesheet" href="style.css"></head><body><div id="container"><div class="smiley-green"><div class="left-eye"></div><div class="right-eye"></div><div class="smile"></div></div><div class="smiley-yellow"><div class="left-eye"></div><div class="right-eye"></div><div class="smile"></div></div><div class="smiley-red"><div class="left-eye"></div><div class="right-eye"></div><div class="smile"></div></div></div><textarea id="log">Events will show up here!</textarea><script src="script.js"></script></body></html>
body,html {margin: 0;padding: 0;}#container {border: 1px solid brown;padding: 10px;width: 330px;margin-bottom: 5px;box-sizing: border-box;}#log {height: 120px;width: 350px;display: block;box-sizing: border-box;}[class^="smiley-"] {display: inline-block;width: 70px;height: 70px;border-radius: 50%;margin-right: 20px;}.smiley-green {background: #a9db7a;border: 5px solid #92c563;position: relative;}.smiley-green .left-eye {width: 18%;height: 18%;background: #84b458;position: relative;top: 29%;left: 22%;border-radius: 50%;float: left;}.smiley-green .right-eye {width: 18%;height: 18%;border-radius: 50%;position: relative;background: #84b458;top: 29%;right: 22%;float: right;}.smiley-green .smile {position: absolute;top: 67%;left: 16.5%;width: 70%;height: 20%;overflow: hidden;}.smiley-green .smile:after,.smiley-green .smile:before {content: "";position: absolute;top: -50%;left: 0%;border-radius: 50%;background: #84b458;height: 100%;width: 97%;}.smiley-green .smile:after {background: #84b458;height: 80%;top: -40%;left: 0%;}.smiley-yellow {background: #eed16a;border: 5px solid #dbae51;position: relative;}.smiley-yellow .left-eye {width: 18%;height: 18%;background: #dba652;position: relative;top: 29%;left: 22%;border-radius: 50%;float: left;}.smiley-yellow .right-eye {width: 18%;height: 18%;border-radius: 50%;position: relative;background: #dba652;top: 29%;right: 22%;float: right;}.smiley-yellow .smile {position: absolute;top: 67%;left: 19%;width: 65%;height: 14%;background: #dba652;overflow: hidden;border-radius: 8px;}.smiley-red {background: #ee9295;border: 5px solid #e27378;position: relative;}.smiley-red .left-eye {width: 18%;height: 18%;background: #d96065;position: relative;top: 29%;left: 22%;border-radius: 50%;float: left;}.smiley-red .right-eye {width: 18%;height: 18%;border-radius: 50%;position: relative;background: #d96065;top: 29%;right: 22%;float: right;}.smiley-red .smile {position: absolute;top: 57%;left: 16.5%;width: 70%;height: 20%;overflow: hidden;}.smiley-red .smile:after,.smiley-red .smile:before {content: "";position: absolute;top: 50%;left: 0%;border-radius: 50%;background: #d96065;height: 100%;width: 97%;}.smiley-red .smile:after {background: #d96065;height: 80%;top: 60%;left: 0%;}
container.onmouseover = container.onmouseout = handler;function handler(event) {function str(el) {if (!el) return "null"return el.className || el.tagName;}log.value += event.type + ': ' +'target=' + str(event.target) +', relatedTarget=' + str(event.relatedTarget) + "\n";log.scrollTop = log.scrollHeight;if (event.type == 'mouseover') {event.target.style.background = 'pink'}if (event.type == 'mouseout') {event.target.style.background = ''}}

relatedTarget 可以为 null
relatedTarget 属性可以为 null。
这是正常现象,仅仅是意味着鼠标不是来自另一个元素,而是来自窗口之外。或者它离开了窗口。
当我们在代码中使用 event.relatedTarget 时,我们应该牢记这种可能性。如果我们访问 event.relatedTarget.tagName,那么就会出现错误。
跳过元素
当鼠标移动时,就会触发 mousemove 事件。但这并不意味着每个像素都会导致一个事件。
浏览器会一直检查鼠标的位置。如果发现了变化,就会触发事件。
这意味着,如果访问者非常快地移动鼠标,那么某些 DOM 元素就可能被跳过:
如果鼠标从上图所示的 #FROM 快速移动到 #TO 元素,则中间的 
这对性能很有好处,因为可能有很多中间元素。我们并不真的想要处理每一个移入和离开的过程。
另一方面,我们应该记住,鼠标指针并不会“访问”所有元素。它可以“跳过”一些元素。
特别是,鼠标指针可能会从窗口外跳到页面的中间。在这种情况下,relatedTarget 为 null,因为它是从石头缝里蹦出来的(nowhere):

你可以在下面的测试台中“实时”查看。
它的 HTML 有两个嵌套的元素:
还可以将鼠标指针移动到
<!doctype html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="style.css"></head><body><div id="parent">parent<div id="child">child</div></div><textarea id="text"></textarea><input onclick="clearText()" value="Clear" type="button"><script src="script.js"></script></body></html>
#parent {background: #99C0C3;width: 160px;height: 120px;position: relative;}#child {background: #FFDE99;width: 50%;height: 50%;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}textarea {height: 140px;width: 300px;display: block;}
let parent = document.getElementById('parent');parent.onmouseover = parent.onmouseout = parent.onmousemove = handler;function handler(event) {let type = event.type;while (type.length < 11) type += ' ';log(type + " target=" + event.target.id)return false;}function clearText() {text.value = "";lastMessage = "";}let lastMessageTime = 0;let lastMessage = "";let repeatCounter = 1;function log(message) {if (lastMessageTime == 0) lastMessageTime = new Date();let time = new Date();if (time - lastMessageTime > 500) {message = '------------------------------\n' + message;}if (message === lastMessage) {repeatCounter++;if (repeatCounter == 2) {text.value = text.value.trim() + ' x 2\n';} else {text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n";}} else {repeatCounter = 1;text.value += message + "\n";}text.scrollTop = text.scrollHeight;lastMessageTime = time;lastMessage = message;}

如果 mouseover 被触发了,则必须有 mouseout
在鼠标快速移动的情况下,中间元素可能会被忽略,但是我们可以肯定一件事:如果鼠标指针“正式地”进入了一个元素(生成了 mouseover 事件),那么一旦它离开,我们就会得到 mouseout。
当移动到一个子元素时 mouseout
mouseout 的一个重要功能 —— 当鼠标指针从元素移动到其后代时触发,例如在下面的这个 HTML 中,从 #parent 到 #child:
<div id="parent"><div id="child">...</div></div>
如果我们在 #parent 上,然后将鼠标指针更深入地移入 #child,在 #parent 上我们会得到 mouseout!
这听起来很奇怪,但很容易解释。
根据浏览器的逻辑,鼠标指针随时可能位于单个元素上 —— 嵌套最多的那个元素(z-index 最大的那个)。
因此,如果它转到另一个元素(甚至是一个后代),那么它将离开前一个元素。
请注意事件处理的另一个重要的细节。
后代的 mouseover 事件会冒泡。因此,如果 #parent 具有 mouseover 处理程序,它将被触发
你可以在下面这个示例中很清晰地看到这一点:
如果你将鼠标从 #parent 移动到 #child,那么你会看到在 #parent 上有两个事件:
- mouseout [target: parent](离开 parent),然后
 mouseover [target: child](来到 child,冒泡)。 ```html <!doctype html>
parentchild
```css#parent {background: #99C0C3;width: 160px;height: 120px;position: relative;}#child {background: #FFDE99;width: 50%;height: 50%;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}textarea {height: 140px;width: 300px;display: block;}
function mouselog(event) {let d = new Date();text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');text.scrollTop = text.scrollHeight;}

如上例所示,当鼠标指针从 #parent 元素移动到 #child 时,会在父元素上触发两个处理程序:mouseout 和 mouseover:
parent.onmouseout = function(event) {/* event.target: parent element */};parent.onmouseover = function(event) {/* event.target: child element (bubbled) */};
如果我们不检查处理程序中的 event.target,那么似乎鼠标指针离开了 #parent 元素,然后立即回到了它上面。
但是事实并非如此!鼠标指针仍然位于父元素上,它只是更深入地移入了子元素。
如果离开父元素时有一些行为(action),例如一个动画在 parent.onmouseout 中运行,当鼠标指针深入 #parent 时,我们并不希望发生这种行为。
为了避免它,我们可以在处理程序中检查 relatedTarget,如果鼠标指针仍在元素内,则忽略此类事件。
另外,我们可以使用其他事件:mouseenter 和 mouseleave,它们没有此类问题,接下来我们就对其进行详细介绍。
事件 mouseenter 和 mouseleave
事件 mouseenter/mouseleave 类似于 mouseover/mouseout。它们在鼠标指针进入/离开元素时触发。
但是有两个重要的区别:
- 元素内部与后代之间的转换不会产生影响。
 - 事件 mouseenter/mouseleave 不会冒泡。
 
这些事件非常简单。
当鼠标指针进入一个元素时 —— 会触发 mouseenter。而鼠标指针在元素或其后代中的确切位置无关紧要。
当鼠标指针离开该元素时,事件 mouseleave 才会触发。
这个例子和上面的例子相似,但是现在最顶部的元素有 mouseenter/mouseleave 而不是 mouseover/mouseout。
正如你所看到的,唯一生成的事件是与将鼠标指针移入或移出顶部元素有关的事件。当鼠标指针进入 child 并返回时,什么也没发生。在后代之间的移动会被忽略。
<!doctype html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="style.css"></head><body><div id="parent" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">parent<div id="child">child</div></div><textarea id="text"></textarea><input type="button" onclick="text.value=''" value="Clear"><script src="script.js"></script></body></html>
#parent {background: #99C0C3;width: 160px;height: 120px;position: relative;}#child {background: #FFDE99;width: 50%;height: 50%;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}textarea {height: 140px;width: 300px;display: block;}
function mouselog(event) {let d = new Date();text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');text.scrollTop = text.scrollHeight;}
事件委托
事件 mouseenter/leave 非常简单且易用。但它们不会冒泡。因此,我们不能使用它们来进行事件委托。
假设我们要处理表格的单元格的鼠标进入/离开。并且这里有数百个单元格。
通常的解决方案是 —— 在 
| 上,那么只有 |  上的处理程序才能捕获到它。 上的 mouseenter/leave 的处理程序仅在鼠标指针进入/离开整个表格时才会触发。无法获取有关其内部移动的任何信息。 因此,让我们使用 mouseover/mouseout。 让我们从高亮显示鼠标指针下的元素的简单处理程序开始: 
 现在它们已经激活了。当鼠标在下面这个表格的各个元素上移动时,当前位于鼠标指针下的元素会被高亮显示: 
 
 
 
 
  | 


