mouseenter与mouseover为何这般纠缠不清?

前言

原文地址

项目地址

不知道大家在面试或者工作过程中有没有被mouseovermouseenter(对应的是mouseoutmouseleave)事件所困扰。自己之前在面试的时候就有被问到诸如mouseover和mouseenter事件的异同之类的问题?当时没有答出来,一直也对这两个事件有点模糊不清,趁着最近正在读zepto源码,准备写一篇这方面的文章,如果有错误,请大家指正。

mouseenter与mouseover的异同?

要说清楚mouseenter与�mouseover有什么不同,也许可以从两方面去讲。

  1. 是否支持冒泡
  2. 事件的触发时机

先来看一张图,对这两个事件有一个简单直观的感受。

再看看官网对mouseenter的解释

mouseenter | onmouseenter event

The event fires only if the mouse pointer is outside the boundaries of the object and the user moves the mouse pointer inside the boundaries of the object. If the mouse pointer is currently inside the boundaries of the object, for the event to fire, the user must move the mouse pointer outside the boundaries of the object and then back inside the boundaries of the object.

大概意思是说:当鼠标从元素的边界之外移入元素的边界之内时,事件被触发。而当鼠标本身在元素边界内时,要触发该事件,必须先将鼠标移出�元素边界外,再次移入才能触发。(英语比较渣😶,凑合看哈)

Unlike the onmouseover event, the onmouseenter event does not bubble.

大概意思是:和mouseover不同的是,mouseenter不�支持事件冒泡 (英语比较渣😶,凑合看哈)

由于mouseenter不支持事件冒泡,导致在一个元素的子元素上进入或离开的时候会触发其mouseover和mouseout事件,但是却不会触发mouseenter和mouseleave事件

我们用一张�动图来看看他们的区别(或者点击该链接体验)。

我们给左右两边的ul分别添加了mouseover�mouseenter事件,当鼠标进入左右两边的�ul时,mouseovermouseenter事件都触发了,但是当移入各自的子元素li的时候,触发了左边ul上的mouseover事件,然而右边ul的mouseenter事件没有被触发。

造成以上现象本质上是mouseenter事件不支持冒泡所致。

如何模拟mouseenter事件。

可见mouseover事件�因其具有冒泡的性质,在子元素内移动的时候,频繁被触发,如果我们不希望如此,可以使用mouseenter事件代替之,但是早期只有ie浏览器支持该事件,虽然现在大多数高级浏览器都支持了mouseenter事件,但是难免会有些兼容问题,所以如果可以自己手动模拟,那就太好了。

关键因素: relatedTarget 要想手动模拟mouseenter事件,需要对mouseover事件触发时的事件对象event属性relatedTarget了解。

  1. relatedTarget事件属性返回与事件的目标节点相关的节点。
  2. 对于mouseover事件来说,该属性是鼠标指针移到目标节点上时所离开的那个节点。
  3. 对于mouseout事件来说,该属性是离开目标时,鼠标指针进入的节点。
  4. 对于其他类型的事件来说,这个属性没有用。

重新回顾一下文章最初的那张图,根据上面的解释,对于ul上添加的mouseover事件来说,relatedTarget只可能是

  1. ul的父元素wrap(移入ul时,此时也是触发mouseenter事件的时候, 其实不一定,后面�会说明),
  2. 或者ul元素本身(在其子元素上移出时),
  3. 又或者是子元素本身(直接从子元素A移动到子元素B)。
relatedTarget

根据上面的描述,�我们可以对relatedTarget的值进行判断:如果值不是目标元素,也不是目标元素的子元素,就说明鼠标已移入目标元素而不是在元素内部移动。

条件1: 不是目标元素很好判断e.relatedTarget !== target(目标元素)

条件2:不是目标元素的子元素,这个应该怎么判断呢?

ele.contains

这里需要介绍一个新的api node.contains(otherNode)
, 表示传入的节点是否为该节点的后代节点, 如果 otherNode 是 node 的后代节点或是 node 节点本身.则返回true , 否则返回 false

用法案例


<ul class="list">
  <li class="item">1</li>
  <li>2</li>
</ul>
<div class="test"></div>

let $list = document.querySelector('.list')
let $item = document.querySelector('.item')
let $test = document.querySelector('.test')

$list.contains($item) // true
$list.contains($test) // false
$list.contains($list) // true

那么利用contains这个api我们便可以很方便的验证条件2,接下来我们封装一个contains(parent, node)函数,专门用来判断node是不是parent的子节点


let contains = function (parent, node) {
  return parent !== node && parent.contains(node)
}


用我们封装过后的contains函数再去试试上面的例子

contains($list, $item) // true
contains($list, $test) // false
contains($list, $list) // false (�主要区别在这里)

�这个方法很方便地帮助我们解决了模拟mouseenter事件中的条件2,但是悲催的�ode.contains(otherNode),具有浏览器兼容性,在一些低级浏览器中是不支持的,为了做到兼容我们再来改写一下contains方法

let contains = docEle.contains ? function (parent, node) {
  return parent !== node && parent.contains(node)
} : function (parent, node) {
  let result = parent !== node

  if (!result) { // 排除parent与node传入相同的节点
    return result
  }

  if (result) {
    while (node && (node = node.parentNode)) {
      if (parent === node) {
        return true
      }
    }
  }

  return false
}

说了这么多,我们来看看用mouseover事件模拟mouseenter的最终代码


// callback表示如果执行mouseenter事件时传入的回调函数

let emulateEnterOrLeave = function (callback) {
  return function (e) {
    let relatedTarget = e.relatedTarget
    if (relatedTarget !== this && !contains(this, relatedTarget)) {
      callback.apply(this, arguments)
    }
  }
}

模拟�mouseenter与原生�mouseenter事件效果对比

html

<div class="wrap">
  wrap, mouseenter
  <ul class="mouseenter list">
    count: <span class="count"></span>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
</div>

<div class="wrap">
  wrap, emulate mouseenter,用mouseover模拟实现mouseenter
  <ul class="emulate-mouseenter list">
    count: <span class="count"></span>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
</div>

css

.wrap{
  width: 50%;
  box-sizing: border-box;
  float: left;
}

.wrap, .list{
  border: solid 1px green;
  padding: 30px;
  margin: 30px 0;
}

.list{
  border: solid 1px red;
}

.list li{
  border: solid 1px blue;
  padding: 10px;
  margin: 10px;
}

.count{
  color: red;
}

javascript

let $mouseenter = document.querySelector('.mouseenter')
let $emulateMouseenter = document.querySelector('.emulate-mouseenter')
let $enterCount = document.querySelector('.mouseenter .count')
let $emulateMouseenterCounter = document.querySelector('.emulate-mouseenter .count')

let addCount = function (ele, start) {
  return function () {
    ele.innerHTML = ++start
  }
}

let docEle = document.documentElement
  let contains = docEle.contains ? function (parent, node) {
    return parent !== node && parent.contains(node)
  } : function (parent, node) {
  let result = parent !== node

  if (!result) {
    return result
  }

  if (result) {
    while (node && (node = node.parentNode)) {
      if (parent === node) {
        return true
      }
    }
  }

  return false
}

let emulateMouseenterCallback = addCount($emulateMouseenterCounter, 0)
  
let emulateEnterOrLeave = function (callback) {
  return function (e) {
    let relatedTarget = e.relatedTarget
    if (relatedTarget !== this && !contains(this, relatedTarget)) {
      callback.apply(this, arguments)
    }
  }
}

$mouseenter.addEventListener('mouseenter', addCount($enterCount, 0), false)
$emulateMouseenter.addEventListener('mouseover', emulateEnterOrLeave(emulateMouseenterCallback), false)  

效果预览

详细代码点击

代码示例点击

好了,我们已经通过mouseove事件完整的模拟了mouseenter事件,但是反过头来看看

对于ul上添加的mouseover事件来说,relatedTarget只可能是

  1. ul的父元素wrap(移入ul时,此时也是触发mouseenter事件的时候, 其实不一定,后面�会说明),
  2. 或者ul元素本身(在其子元素上移出时),
  3. 又或者是子元素本身(直接从子元素A移动到子元素B)。

我们通过排查2和3,最后只留下1,也就是mouseenter与�mouseover�事件一起触发的时机。既然这样我们为什么不像这样判断呢?

�target.addEventListener('mouseover', function (e) {
  if (e.relatedTarget === this.parentNode) {
    // 执行mouseenter的回调要做的事情  
  }
}, false)


这样不是更加简单吗?,何必要折腾通过排查2和3来做?

原因是,�target的父元素有一定的占位空间的时后,我们这样写是没有太大问题的,但是反之,这个时候e.relatedTarget就可能是target元素的父元素,又祖先元素中的某一个。我们无法准确判断e.relatedTarget到底是哪个元素。所以通过排除2和3应该是个更好的选择。

用mouseout模拟mouseleave事件

当mouseout被激活时,relatedTarget表示鼠标离开目标元素时,进入了哪个元素,我们同样可以对relatedTarget的值进行判断:如果值不是目标元素,也不是目标元素的子元素,就说明鼠标已移出目标元素

我们同样可以用上面封装的函数完成


// callback表示如果执行mouseenter事件时传入的回调函数

let emulateEnterOrLeave = function (callback) {
  return function (e) {
    let relatedTarget = e.relatedTarget
    if (relatedTarget !== this && !contains(this, relatedTarget)) {
      callback.apply(this, arguments)
    }
  }
}

详细代码点击

代码示例点击

结尾

文中也许有些观点不够严谨,欢迎大家拍砖。

原文地址

项目地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容