web前端 对鼠标悬浮事件mouseover和mouseenter的机制研究-捕获和冒泡

497 阅读3分钟

事件的捕获和冒泡应该是js DOM的基础内容了,但是你真的了解mouseover和mouseenter事件的机制吗? 这里做一个很简单的实验

<body>
   <div id="d1">
      d1
      <div id="d2">
         d2
      </div>
   </div>
   <script>
      var d1 = document.getElementById('d1')
      var d2 = document.getElementById('d2')
      d1.addEventListener('mouseover', function () {
         console.log('d1');
      })
      d2.addEventListener('mouseover', function (event) {
         console.log('d2');
      })
   </script>
</body>

当我们把鼠标移入div1 看到正常打印了d1

> d1

然后我们把鼠标移入div2 控制台先打印了d2然后打印了d1

> d2
> d1

这好像不符合我们的正常思维 为什么会这样呢? 因为事件冒泡了,事件向上传递到了div1 但是有时候我们可能不需要这样的冒泡 mouseenter的存在就是为了这种需求 当我们在div1和div2之间来回移动,d1永远只打印一次 而mouseover会在你每次移入div1的时候都打印一次,它其实是解决了当结构复杂的元素需要绑定一个移入事件时的由冒泡造成的重复执行的问题。 但是这一招在多层移入事件时并不好用,因为有时候我们希望当我们把鼠标移入div2时只打印d2然后停止冒泡。但事实上是我们把鼠标移入div2时也打印了d1。也就是说事件依然出现了冒泡。那我们加个event.stopPropagation()呢?

> d1
> d2

并不好使。 因为在MDN规范里,mouseenter是不能冒泡的,既然不能冒泡那自然就不能取消冒泡,但其实它确实会出现冒泡,只不过这个冒泡的规则被代理了。 所以如果你是单纯的需要阻止事件冒泡,还是要用mouseover事件,然后用event.stopPropagation()阻止冒泡 但是这样还是不能满足我们的需求,因为当我们从div1移动到div2内,其实我们并没有移出div1,但是我们的移出事件依然会执行,然后又因为因为事件的冒泡再次执行移入事件,但是如果我们取消了冒泡的话,div1被移出之后就回不来了,这明显不够人性化。 要解决这个问题首先要了解mouseleave的工作原理。 这是MDN官网的mouseleave工作原理,我在此基础上做了一些利于阅读的改变

var element // 绑定事件的元素
element.addEventListener("mouseout", function (event) {
   var target = event.target, // 事件的主要目标
      related = event.relatedTarget, // 事件的次要目标
      match; //选择器是否匹配

   // 搜索匹配委托选择器的父节点
   while (target && target != document && !(match = element.matches(target))) {
      target = target.parentNode;
   }

   // 如果没有找到匹配的节点,则退出
   if (!match) { return; }

   // 遍历相关目标的父级以确保它不是目标的子级
   while (related && related != target && related != document) {
      related = related.parentNode;
   }

   // 如果是这种情况就退出
   if (related == target) { return; }

   // 现在可以执行“委托的鼠标离开”处理程序
   // 改变文字的颜色
   target.style.color = "orange";
   // 一段时间后重置颜色
   setTimeout(function () {
      target.style.color = "";
   }, 500);
}, false);

要看懂这个东西,首先要知道两个关键的event属性 target 事件触发的主要源对象 target很好理解,比方说当我们鼠标移入了div2,那么target就是div2,无论是对于div2还是对于div1都是如此,它让我们很容易区分事件究竟是自己触发的还是由子元素冒泡触发的 relatedTarget 事件触发的次要源对象 这么说有些抽象,不妨做个例子:假设我们把鼠标从div2移到div1,那么div2就会触发移出事件,此时事件的relatedTarget就是div1,就好像是再告诉它,“鼠标从你身上移走了,为什么移走了呢?因为移到div1去了”。但是它也有可能会是空的,那就是在鼠标从浏览器页面之外移入或者是移到了页面外的时候。 知道了这些,就有了重现mouseleave的条件。 原理就是当鼠标移出事件触发时,我们去看鼠标移到哪去了,如果是移到了自己或自己的后代元素,那就不需要触发移出事件。 简单的写起来像是这样子:

d1.addEventListener('mouseout', function (event) {
   let target = event.relatedTarget
   while (target) {
      if (this.isEqualNode(target)) {
         return
      }
      target = target.parentNode
   }
   console.log('d1 移出');
})