原文地址javascript
项目地址css
不知道你们在面试或者工做过程当中有没有被
mouseover
和mouseenter
(对应的是mouseout
和mouseleave
)事件所困扰。本身以前在面试的时候就有被问到诸如mouseover和mouseenter事件的异同之类的问题?当时没有答出来,一直也对这两个事件有点模糊不清,趁着最近正在读zepto源码,准备写一篇这方面的文章,若是有错误,请你们指正。html
<!--more-->java
要说清楚mouseenter与mouseover有什么不一样,也许能够从两方面去讲。node
是否支持冒泡git
事件的触发时机github
先来看一张图,对这两个事件有一个简单直观的感觉。面试
再看看官网对mouseenter的解释api
mouseenter | onmouseenter event.aspx)浏览器
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时,mouseover
和mouseenter
事件都触发了,可是当移入各自的子元素li的时候,触发了左边ul上的mouseover事件,然而右边ul的mouseenter事件没有被触发。
形成以上现象本质上是mouseenter
事件不支持冒泡所致。
可见mouseover事件因其具备冒泡的性质,在子元素内移动的时候,频繁被触发,若是咱们不但愿如此,可使用mouseenter事件代替之,可是早期只有ie浏览器支持该事件,虽然如今大多数高级浏览器都支持了mouseenter事件,可是不免会有些兼容问题,因此若是能够本身手动模拟,那就太好了。
关键因素: relatedTarget 要想手动模拟mouseenter事件,须要对mouseover事件触发时的事件对象event属性relatedTarget了解。
relatedTarget事件属性返回与事件的目标节点相关的节点。
对于mouseover事件来讲,该属性是鼠标指针移到目标节点上时所离开的那个节点。
对于mouseout事件来讲,该属性是离开目标时,鼠标指针进入的节点。
对于其余类型的事件来讲,这个属性没有用。
从新回顾一下文章最初的那张图,根据上面的解释,对于ul上添加的mouseover事件来讲,relatedTarget只多是
ul的父元素wrap(移入ul时,此时也是触发mouseenter事件的时候, 其实不必定,后面会说明),
或者ul元素自己(在其子元素上移出时),
又或者是子元素自己(直接从子元素A移动到子元素B)。
根据上面的描述,咱们能够对relatedTarget的值进行判断:若是值不是目标元素,也不是目标元素的子元素,就说明鼠标已移入目标元素而不是在元素内部移动。
条件1: 不是目标元素很好判断e.relatedTarget !== target(目标元素)
条件2:不是目标元素的子元素,这个应该怎么判断呢?
这里须要介绍一个新的api [node.contains(otherNode)
](https://developer.mozilla.org... 表示传入的节点是否为该节点的后代节点, 若是 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只多是
ul的父元素wrap(移入ul时,此时也是触发mouseenter事件的时候, 其实不必定,后面会说明),
或者ul元素自己(在其子元素上移出时),
又或者是子元素自己(直接从子元素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被激活时,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) } } }
文中也许有些观点不够严谨,欢迎你们拍砖。