天天看點

mouseenter與mouseover為何這般糾纏不清?

前言

項目位址

不知道大家在面試或者工作過程中有沒有被

mouseover

mouseenter

(對應的是

mouseout

mouseleave

)事件所困擾。自己之前在面試的時候就有被問到諸如mouseover和mouseenter事件的異同之類的問題?當時沒有答出來,一直也對這兩個事件有點模糊不清,趁着最近正在讀zepto源碼,準備寫一篇這方面的文章,如果有錯誤,請大家指正。
mouseenter與mouseover為何這般糾纏不清?

<!--more-->

mouseenter與mouseover的異同?

要說清楚mouseenter與mouseover有什麼不同,也許可以從兩方面去講。
  1. 是否支援冒泡
  2. 事件的觸發時機

先來看一張圖,對這兩個事件有一個簡單直覺的感受。

mouseenter與mouseover為何這般糾纏不清?

再看看官網對mouseenter的解釋

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事件

我們用一張動圖來看看他們的差別(或者點選該連結體驗)。

mouseenter與mouseover為何這般糾纏不清?

我們給左右兩邊的ul分别添加了

mouseover

mouseenter

事件,當滑鼠進入左右兩邊的ul時,

mouseover

mouseenter

事件都觸發了,但是當移入各自的子元素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)。
mouseenter與mouseover為何這般糾纏不清?

根據上面的描述,我們可以對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)) {
      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 = ++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)) {
      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)           

複制

效果預覽

mouseenter與mouseover為何這般糾纏不清?

詳細代碼點選

代碼示例點選

好了,我們已經通過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) {
    // 執行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)
    }
  }
}           

複制

詳細代碼點選

代碼示例點選

結尾

文中也許有些觀點不夠嚴謹,歡迎大家拍磚。