事件mouseover / mouseout, relatedTarget

一、当鼠标指针移到某个元素上时,mouseover事件就会发生,而当鼠标离开该元素时,mouseout事件就会发生。
image.png
二、mouseover、 mouseout具有relatedTarget属性。此属性是对target的补充。
1、当鼠标从一个元素离开并去往另一个元素时,其中一个元素就变成了target,另一个就变成了relatedTarget
(1)mouseover
① event.target:是鼠标移过的那个元素
② event.relatedTarget:是鼠标来自的那个元素(relatedTarget -> target)
(2)mouseout
① event.target:鼠标离开的元素
② event.relatedTarget:鼠标移动到的,当前指针位置下的元素(target -> relatedTarget)
2、relatedTarget可以为null。这意味着鼠标不是来自另一个元素,而是来自窗口之外,或者它离开了窗口。

跳过元素

一、当鼠标移动时,就会触发mousemove事件。但这并不意味着每个像素都会导致一个事件。
1、浏览器会一直检查鼠标的位置。如果发生了变化,就会触发事件。
2、这意味着,如果访问者非常快地移动鼠标,那么某些DOM元素就可能被跳过。
【示例1】如果鼠标从上图所示的#FORM快速移动到#T0元素,则中间的

(或其中的一些)元素可能回被跳过。mouseout事件可能会在#FORM上被触发,然后立即在#TO上触发mouseover。
image.png
3、这对性能很有好处,因为可能会有很多中间元素。我们并不真的想要处理每一个移入和离开的过程。
二、鼠标指针并不会“访问”所有元素。它可以“跳过”一些元素。
1、鼠标指针可能会从窗口外跳到页面的中间。在这种情况下,relatedTarget为null。
image.png
三、在鼠标快速移动的情况下,中间元素可能会被忽略,但是,如果鼠标进入了一个元素(生成了mouseover事件),那么一旦它离开,我们就会得到mouseout

当移动到一个子元素时mouseout

一、mouseout的一个重要功能:当鼠标指针从元素移动到其后代时触发。
【示例1】如果我们在#parent上,然后将鼠标指针更深入地移入#child,但是在#parent上会得到mouseout
image.png
1、根据浏览器的逻辑,鼠标指针随时可能位于单个元素上,嵌套最多的那个元素(z-index最大的那个)。如果它转到另一个元素(甚至是一个后代),那么它将离开前一个元素。
2、后代的mouseover事件会冒泡。因此,如果#parent具有mouseover处理程序,它将被触发
【示例1】鼠标指针从#parent元素移动到#child时,会在父元素上触发两个处理程序: mouseout 和mouseover
image.png

  1. parent.onmouseout = function(event) {
  2. /* event.target = parent element */
  3. }
  4. parent.onmouseover = function(event) {
  5. /* event.target = child elment (bubbled) */
  6. }

(1)如果我们不检查处理程序中的event.target,那么似乎鼠标指针离开了#parent元素,然后立即回到了它上面。但事实并非如此,鼠标指针仍然位于父元素上,只是更深入地移入了子元素。
【示例1】如果离开父元素时有一些行为(action),例如一个动画在parent.onmouseout中运行,当鼠标指针深入#parent时,我们并不希望发生这种行为。为了避免它,我们可以在处理程序中检查relatedTarget,如果鼠标指针仍然在元素内,则忽略此类事件。
二、mouseenter,mouseleave,它们没有此类问题。

事件mouseenter 和mouseleave

一、事件mouseenter / mouseleave 类似于mouseover / mouseout。它们在鼠标指针进入 / 离开元素时触发。
1、区别
(1)元素内部与后代之间的转换不会产生影响。
(2)事件mouseenter / mouseleave不会冒泡。
二、当鼠标指针进入一个元素时,会触发mouseenter。而鼠标指针在元素或其后代中的确切位置无关紧要。
当鼠标指针离开该元素时,事件mouseleave才会触发。
【示例1】鼠标指针进入或移出#parent,会触发mouseenter, mouseleave,当鼠标指针进入child并返回时,不会触发#parent的这两个事件。在后代之间的移动会被忽略。

<div id="parent" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">parent
  <div id="child">child</div>
</div>

<script>
  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;
  }
</script>

事件委托

一、事件mouseenter / mouseleave不会冒泡。因此,我们不能使用它们来进行事件委托。
【示例1】假设要处理表格的单元格的鼠标进入 / 离开。并且这里有数百个单元格。
1、解决方案:通常解决方案是

中设置处理程序,并在那里处理事件。但mouseenter / leave不会冒泡。因此,如果类似的事件发生在
上,那么只有上的处理程序才能捕获到它。上的mouseenter /leave的处理程序仅在鼠标指针进入/离开整个表格时才会触发。无法获取有关其内部移动的任何信息。
因此,我们使用mouseover / mouseout
2、高亮显示鼠标指针下的元素

// 高亮显示鼠标指针下的元素
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';
};

3、我们想要处理表格的单元格

之间的移动:进入一个单元格并离开它。所以我们需要过滤其他移动,如在单元格内部或在所有单元格的外部。
(1)思路
① 在变量中记住当前被高亮显示的
,让我们称它为currentElem
② mouseover:如果我们仍然在当前的
中,则忽略该事件。
③ mouseout:如果没有离开当前的
,则忽略。
(2)实现代码

<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>
// 现在位于鼠标下方的 <td>(如果有)
let currentElem = null;

table.onmouseover = function(event) {
  // 在进入一个新的元素前,鼠标总是会先离开前一个元素
  // 如果设置了 currentElem,那么我们就没有鼠标所悬停在的前一个 <td>,
  // 忽略此事件
  if (currentElem) return;

  let target = event.target.closest('td');

  // 我们移动到的不是一个 <td> —— 忽略
  if (!target) return;

  // 现在移动到了 <td> 上,但在处于了我们表格的外部(可能因为是嵌套的表格)
  // 忽略
  if (!table.contains(target)) return;

  // 给力!我们进入了一个新的 <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // 如果我们现在处于所有 <td> 的外部,则忽略此事件
  // 这可能是一个表格内的移动,但是在 <td> 外,
  // 例如从一个 <tr> 到另一个 <tr>
  if (!currentElem) return;

  // 我们将要离开这个元素 —— 去哪儿?可能是去一个后代?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // 到父链上并检查 —— 我们是否还在 currentElem 内
    // 然后发现,这只是一个内部移动 —— 忽略它
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // 我们离开了 <td>。真的。
  onLeave(currentElem);
  currentElem = null;
};

// 任何处理进入/离开一个元素的函数
function onEnter(elem) {
  elem.style.background = 'pink';

  // 在文本区域显示它
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = '';

  // 在文本区域显示它
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

4、上述代码实现的功能:
(1)使用事件委托来处理表格中任何

的进入/ 离开。因此,它依赖于mouseover / mouseout 而不是mouseenter / leave,mouseenter /leave不会冒泡,因此也不允许事件委托。
(2)额外的事件,如在
的后代之间移动都会被过滤掉,因此onEnter / Leave仅在鼠标进入 / 离开整体时才会运行。