让我们深入了解鼠标在元素之间移动时发生的事件的更多详细信息。
事件 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
的信息
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 = ''
}
}
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%;
}
<!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>
relatedTarget
可以为 null
relatedTarget
属性可以为 null
。
这是正常的,只是表示鼠标不是来自另一个元素,而是来自窗口外。或者它离开了窗口。
在我们的代码中使用 event.relatedTarget
时,我们应该记住这种可能性。如果我们访问 event.relatedTarget.tagName
,那么将出现错误。
跳过元素
当鼠标移动时,mousemove
事件触发。但这并不意味着每个像素都会导致一个事件。
浏览器会时不时地检查鼠标位置。如果它注意到变化,则会触发事件。
这意味着,如果访问者移动鼠标的速度非常快,那么可能会跳过一些 DOM 元素
如果鼠标从 #FROM
元素非常快速地移动到 #TO
元素(如上所述),那么中间的 <div>
元素(或其中一些元素)可能会被跳过。mouseout
事件可能会在 #FROM
上触发,然后立即在 #TO
上触发 mouseover
。
这对性能有好处,因为可能有很多中间元素。我们并不真正想处理每个元素的输入和输出。
另一方面,我们应该记住,鼠标指针不会“访问”沿途的所有元素。它可以“跳跃”。
特别是,指针有可能直接从窗口外跳到页面中间。在这种情况下,relatedTarget
为 null
,因为它来自“无处”。
您可以在下面的测试台上“实时”查看它。
它的 HTML 有两个嵌套元素:<div id="child">
在 <div id="parent">
中。如果您快速将鼠标移动到它们上面,那么可能只有子 div 触发事件,或者可能是父 div,或者可能根本没有事件。
还将指针移入子 div
,然后快速向下穿过父 div 移出。如果移动速度足够快,那么父元素将被忽略。鼠标将越过父元素而不注意它。
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;
}
#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;
}
<!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>
mouseover
,则必须有 mouseout
在鼠标快速移动的情况下,可能会忽略中间元素,但有一件事我们可以肯定:如果指针“正式”进入了一个元素(生成了 mouseover
事件),那么在离开它时,我们总是会得到 mouseout
。
离开子元素时的 Mouseout
mouseout
的一个重要特征 - 当指针从一个元素移动到它的后代时,它会触发,例如,从这个 HTML 中的 #parent
到 #child
<div id="parent">
<div id="child">...</div>
</div>
如果我们在 #parent
上,然后将指针更深入地移动到 #child
中,我们会在 #parent
上获得 mouseout
!
这可能看起来很奇怪,但可以轻松解释。
根据浏览器逻辑,鼠标光标在任何时候只能在单个元素上——最嵌套的元素和 z 轴索引最高的元素。
因此,如果它转到另一个元素(甚至是后代),则它会离开前一个元素。
请注意事件处理的另一个重要细节。
后代上的 mouseover
事件会冒泡。因此,如果 #parent
有 mouseover
处理程序,它将触发
您可以在下面的示例中很好地看到这一点:<div id="child">
在 <div id="parent">
中。#parent
元素上有 mouseover/out
处理程序,用于输出事件详细信息。
如果您将鼠标从 #parent
移动到 #child
,您将在 #parent
上看到两个事件
mouseout [target: parent]
(离开了父级),然后mouseover [target: child]
(进入子级,冒泡)。
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 {
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;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="parent" onmouseover="mouselog(event)" onmouseout="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
元素移动到 #child
时,父元素上会触发两个处理程序:mouseout
和 mouseover
parent.onmouseout = function(event) {
/* event.target: parent element */
};
parent.onmouseover = function(event) {
/* event.target: child element (bubbled) */
};
如果我们在处理程序中不检查 event.target
,那么看起来鼠标指针离开了 #parent
元素,然后立即又返回到它上面。
但这并不是这种情况!指针仍然在父级上,它只是更深入地移动到了子元素中。
如果在离开父元素时有一些操作,例如在 parent.onmouseout
中运行动画,当指针只是更深入地进入 #parent
时,我们通常不希望它发生。
为了避免这种情况,我们可以在处理程序中检查 relatedTarget
,如果鼠标仍然在元素内,则忽略此类事件。
或者,我们可以使用其他事件:mouseenter
和 mouseleave
,我们现在将介绍它们,因为它们没有这样的问题。
事件 mouseenter 和 mouseleave
事件 mouseenter/mouseleave
类似于 mouseover/mouseout
。当鼠标指针进入/离开元素时,它们会触发。
但有两个重要的区别
- 元素内部的转换,到/从后代,不被计算在内。
- 事件
mouseenter/mouseleave
不冒泡。
这些事件非常简单。
当指针进入元素时——触发 mouseenter
。指针在元素或其后代中的确切位置无关紧要。
当指针离开元素时——触发 mouseleave
。
此示例类似于上面的示例,但现在顶级元素具有 mouseenter/mouseleave
而不是 mouseover/mouseout
。
如您所见,唯一生成的事件与将指针移入和移出顶级元素有关。当指针进入子级并返回时,不会发生任何事情。后代之间的转换被忽略
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 {
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;
}
<!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>
事件委托
事件mouseenter/leave
非常简单且易于使用。但它们不会冒泡。因此,我们无法将事件委托与它们一起使用。
想象一下,我们希望处理表格单元格的鼠标进入/离开。并且有数百个单元格。
自然而然的解决方案是——在<table>
上设置处理程序并在那里处理事件。但mouseenter/leave
不会冒泡。因此,如果此类事件发生在<td>
上,那么只有该<td>
上的处理程序才能捕获它。
<table>
上mouseenter/leave
的处理程序仅在指针进入/离开整个表格时触发。不可能获得有关其内部转换的任何信息。
因此,让我们使用mouseover/mouseout
。
让我们从突出显示鼠标下元素的简单处理程序开始
// let's highlight an element under the pointer
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
};
它们正在发挥作用。当鼠标在该表格的元素上移动时,当前元素将被突出显示
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
text.value += `over -> ${target.tagName}\n`;
text.scrollTop = text.scrollHeight;
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
text.value += `out <- ${target.tagName}\n`;
text.scrollTop = text.scrollHeight;
};
#text {
display: block;
height: 100px;
width: 456px;
}
#table th {
text-align: center;
font-weight: bold;
}
#table td {
width: 150px;
white-space: nowrap;
text-align: center;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 12px;
cursor: pointer;
}
#table .nw {
background: #999;
}
#table .n {
background: #03f;
color: #fff;
}
#table .ne {
background: #ff6;
}
#table .w {
background: #ff0;
}
#table .c {
background: #60c;
color: #fff;
}
#table .e {
background: #09f;
color: #fff;
}
#table .sw {
background: #963;
color: #fff;
}
#table .s {
background: #f60;
color: #fff;
}
#table .se {
background: #0c3;
color: #fff;
}
#table .highlight {
background: red;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<table id="table">
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong>
<br>Metal
<br>Silver
<br>Elders
</td>
<td class="n"><strong>North</strong>
<br>Water
<br>Blue
<br>Change
</td>
<td class="ne"><strong>Northeast</strong>
<br>Earth
<br>Yellow
<br>Direction
</td>
</tr>
<tr>
<td class="w"><strong>West</strong>
<br>Metal
<br>Gold
<br>Youth
</td>
<td class="c"><strong>Center</strong>
<br>All
<br>Purple
<br>Harmony
</td>
<td class="e"><strong>East</strong>
<br>Wood
<br>Blue
<br>Future
</td>
</tr>
<tr>
<td class="sw"><strong>Southwest</strong>
<br>Earth
<br>Brown
<br>Tranquility
</td>
<td class="s"><strong>South</strong>
<br>Fire
<br>Orange
<br>Fame
</td>
<td class="se"><strong>Southeast</strong>
<br>Wood
<br>Green
<br>Romance
</td>
</tr>
</table>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script src="script.js"></script>
</body>
</html>
在我们的案例中,我们希望处理表格单元格<td>
之间的转换:进入单元格和离开单元格。其他转换(例如在单元格内或任何单元格外)对我们来说并不重要。让我们将它们筛选出来。
我们可以这样做
- 记住当前突出显示的
<td>
在一个变量中,我们称之为currentElem
。 - 在
mouseover
上——如果我们仍处于当前<td>
内部,则忽略该事件。 - 在
mouseout
上——如果我们没有离开当前<td>
,则忽略。
以下是一个考虑所有可能情况的代码示例
// <td> under the mouse right now (if any)
let currentElem = null;
table.onmouseover = function(event) {
// before entering a new element, the mouse always leaves the previous one
// if currentElem is set, we didn't leave the previous <td>,
// that's a mouseover inside it, ignore the event
if (currentElem) return;
let target = event.target.closest('td');
// we moved not into a <td> - ignore
if (!target) return;
// moved into <td>, but outside of our table (possible in case of nested tables)
// ignore
if (!table.contains(target)) return;
// hooray! we entered a new <td>
currentElem = target;
onEnter(currentElem);
};
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
// that's probably a move inside the table, but out of <td>,
// e.g. from <tr> to another <tr>
if (!currentElem) return;
// we're leaving the element – where to? Maybe to a descendant?
let relatedTarget = event.relatedTarget;
while (relatedTarget) {
// go up the parent chain and check – if we're still inside currentElem
// then that's an internal transition – ignore it
if (relatedTarget == currentElem) return;
relatedTarget = relatedTarget.parentNode;
}
// we left the <td>. really.
onLeave(currentElem);
currentElem = null;
};
// any functions to handle entering/leaving an element
function onEnter(elem) {
elem.style.background = 'pink';
// show that in textarea
text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
text.scrollTop = 1e6;
}
function onLeave(elem) {
elem.style.background = '';
// show that in textarea
text.value += `out <- ${elem.tagName}.${elem.className}\n`;
text.scrollTop = 1e6;
}
再次强调,重要特性是
- 它使用事件委托来处理表格中任何
<td>
的进入/离开。因此,它依赖于mouseover/out
而不是mouseenter/leave
,后者不会冒泡,因此不允许委托。 - 其他事件(例如在
<td>
的后代之间移动)被筛选掉,以便仅当指针离开或进入整个<td>
时,onEnter/Leave
才会运行。
以下是包含所有详细信息的完整示例
// <td> under the mouse right now (if any)
let currentElem = null;
table.onmouseover = function(event) {
// before entering a new element, the mouse always leaves the previous one
// if currentElem is set, we didn't leave the previous <td>,
// that's a mouseover inside it, ignore the event
if (currentElem) return;
let target = event.target.closest('td');
// we moved not into a <td> - ignore
if (!target) return;
// moved into <td>, but outside of our table (possible in case of nested tables)
// ignore
if (!table.contains(target)) return;
// hooray! we entered a new <td>
currentElem = target;
onEnter(currentElem);
};
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
// that's probably a move inside the table, but out of <td>,
// e.g. from <tr> to another <tr>
if (!currentElem) return;
// we're leaving the element – where to? Maybe to a descendant?
let relatedTarget = event.relatedTarget;
while (relatedTarget) {
// go up the parent chain and check – if we're still inside currentElem
// then that's an internal transition – ignore it
if (relatedTarget == currentElem) return;
relatedTarget = relatedTarget.parentNode;
}
// we left the <td>. really.
onLeave(currentElem);
currentElem = null;
};
// any functions to handle entering/leaving an element
function onEnter(elem) {
elem.style.background = 'pink';
// show that in textarea
text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
text.scrollTop = 1e6;
}
function onLeave(elem) {
elem.style.background = '';
// show that in textarea
text.value += `out <- ${elem.tagName}.${elem.className}\n`;
text.scrollTop = 1e6;
}
#text {
display: block;
height: 100px;
width: 456px;
}
#table th {
text-align: center;
font-weight: bold;
}
#table td {
width: 150px;
white-space: nowrap;
text-align: center;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 12px;
cursor: pointer;
}
#table .nw {
background: #999;
}
#table .n {
background: #03f;
color: #fff;
}
#table .ne {
background: #ff6;
}
#table .w {
background: #ff0;
}
#table .c {
background: #60c;
color: #fff;
}
#table .e {
background: #09f;
color: #fff;
}
#table .sw {
background: #963;
color: #fff;
}
#table .s {
background: #f60;
color: #fff;
}
#table .se {
background: #0c3;
color: #fff;
}
#table .highlight {
background: red;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<table id="table">
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong>
<br>Metal
<br>Silver
<br>Elders
</td>
<td class="n"><strong>North</strong>
<br>Water
<br>Blue
<br>Change
</td>
<td class="ne"><strong>Northeast</strong>
<br>Earth
<br>Yellow
<br>Direction
</td>
</tr>
<tr>
<td class="w"><strong>West</strong>
<br>Metal
<br>Gold
<br>Youth
</td>
<td class="c"><strong>Center</strong>
<br>All
<br>Purple
<br>Harmony
</td>
<td class="e"><strong>East</strong>
<br>Wood
<br>Blue
<br>Future
</td>
</tr>
<tr>
<td class="sw"><strong>Southwest</strong>
<br>Earth
<br>Brown
<br>Tranquility
</td>
<td class="s"><strong>South</strong>
<br>Fire
<br>Orange
<br>Fame
</td>
<td class="se"><strong>Southeast</strong>
<br>Wood
<br>Green
<br>Romance
</td>
</tr>
</table>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script src="script.js"></script>
</body>
</html>
尝试将光标移入和移出表格单元格以及在其中移动。快或慢——无关紧要。与之前的示例不同,只有整个<td>
会被突出显示。
总结
我们介绍了事件mouseover
、mouseout
、mousemove
、mouseenter
和mouseleave
。
需要注意以下事项
- 快速移动鼠标可能会跳过中间元素。
- 事件
mouseover/out
和mouseenter/leave
具有一个附加属性:relatedTarget
。这是我们来自/前往的元素,与target
互补。
即使我们从父元素转到子元素,事件mouseover/out
也会触发。浏览器假设鼠标一次只能在一个元素上——最深的那个。
事件mouseenter/leave
在这方面有所不同:它们仅在鼠标进入和离开整个元素时触发。它们也不会冒泡。
评论
<code>
标记,对于多行代码 – 将其包装在<pre>
标记中,对于 10 行以上的代码 – 使用沙盒 (plnkr,jsbin,codepen…)